1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
|
{- hpodder component
Copyright (C) 2006-2008 John Goerzen <jgoerzen@complete.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-}
{- |
Module : DB
Copyright : Copyright (C) 2006-2008 John Goerzen
License : GNU GPL, version 2 or above
Maintainer : John Goerzen <jgoerzen@complete.org>
Stability : provisional
Portability: portable
Written by John Goerzen, jgoerzen\@complete.org
-}
module DB where
import Config
import Types
import Database.HDBC
import Database.HDBC.Sqlite3
import System.Log.Logger
import Control.Monad
import Control.Exception
import Utils
import Data.List.Utils
dbdebug = debugM "DB"
connect :: IO Connection
connect = handleSqlError $
do fp <- getDBName
dbh <- connectSqlite3 fp
setBusyTimeout dbh 5000
prepDB dbh
dbdebug $ "DB preparation complete"
return dbh
prepDB dbh =
do tables <- getTables dbh
evaluate (length tables)
schemaver <- prepSchema dbh tables
upgradeSchema dbh schemaver tables
prepSchema :: Connection -> [String] -> IO Int
prepSchema dbh tables =
if "schemaver" `elem` tables
then do r <- quickQuery dbh "SELECT version FROM schemaver" []
case r of
[[x]] -> return (fromSql x)
x -> fail $ "Unexpected result in prepSchema: " ++ show x
else do dbdebug "Initializing schemaver to 0"
run dbh "CREATE TABLE schemaver (version INTEGER)" []
run dbh "INSERT INTO schemaver VALUES (0)" []
commit dbh
return 0
upgradeSchema dbh 5 _ = return ()
upgradeSchema dbh 4 tables =
do dbdebug "Upgrading schema 4 -> 5"
dbdebug "Recreating episodes table to add epguid column and UNIQUE constaint"
-- Silly sqlite can't add a UNIQUE constaint to an existing table, so we
-- have to recreate it.
run dbh "CREATE TABLE episodes5 (\
\castid INTEGER NOT NULL,\
\episodeid INTEGER NOT NULL,\
\title TEXT NOT NULL,\
\epurl TEXT NOT NULL,\
\enctype TEXT NOT NULL,\
\status TEXT NOT NULL,\
\eplength INTEGER NOT NULL DEFAULT 0,\
\epfirstattempt INTEGER,\
\eplastattempt INTEGER,\
\epfailedattempts INTEGER NOT NULL DEFAULT 0,\
\epguid TEXT,\
\UNIQUE(castid, epurl),\
\UNIQUE(castid, episodeid),\
\UNIQUE(castid, epguid))" []
dbdebug "Copying data from old episodes table"
run dbh "INSERT INTO episodes5 SELECT *, NULL FROM episodes" []
dbdebug "Dropping old episodes table"
run dbh "DROP TABLE episodes" []
dbdebug "Renaming new episodes table"
run dbh "ALTER TABLE episodes5 RENAME TO episodes" []
setSchemaVer dbh 5
commit dbh
upgradeSchema dbh 5 tables
upgradeSchema dbh 3 tables =
do dbdebug "Upgrading schema 3 -> 4"
dbdebug "Adding lastattempt column"
run dbh "ALTER TABLE podcasts ADD lastattempt INTEGER" []
dbdebug "Adding failedattempts column"
run dbh "ALTER TABLE podcasts ADD failedattempts INTEGER NOT NULL DEFAULT 0" []
dbdebug "Adding epfirstattempt column"
run dbh "ALTER TABLE episodes ADD epfirstattempt INTEGER" []
dbdebug "Adding eplastattempt column"
run dbh "ALTER TABLE episodes ADD eplastattempt INTEGER" []
dbdebug "Adding epfailedattempts column"
run dbh "ALTER TABLE episodes ADD epfailedattempts INTEGER NOT NULL DEFAULT 0" []
setSchemaVer dbh 4
commit dbh
upgradeSchema dbh 4 tables
upgradeSchema dbh 2 tables =
do dbdebug "Upgrading schema 2 -> 3"
dbdebug "Adding eplength column"
run dbh "ALTER TABLE episodes ADD eplength INTEGER NOT NULL DEFAULT 0" []
setSchemaVer dbh 3
commit dbh
-- Empty the enclosure storage since our naming changed when this
-- version arrived
edir <- getEnclTmp
emptyDir edir
upgradeSchema dbh 3 tables
upgradeSchema dbh 1 tables =
do dbdebug "Upgrading schema 1 -> 2"
dbdebug "Adding pcenabled column"
run dbh "ALTER TABLE podcasts ADD pcenabled INTEGER NOT NULL DEFAULT 1" []
dbdebug "Adding lastupdate column"
run dbh "ALTER TABLE podcasts ADD lastupdate INTEGER" []
setSchemaVer dbh 2
commit dbh
-- dbdebug "Vacuuming"
-- run dbh "VACUUM" []
upgradeSchema dbh 2 tables
upgradeSchema dbh 0 tables =
do dbdebug "Upgrading schema 0 -> 1"
unless ("podcasts" `elem` tables)
(run dbh "CREATE TABLE podcasts(\
\castid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\
\castname TEXT NOT NULL,\
\feedurl TEXT NOT NULL UNIQUE)" [] >> return ())
unless ("episodes" `elem` tables)
(run dbh "CREATE TABLE episodes (\
\castid INTEGER NOT NULL, \
\episodeid INTEGER NOT NULL, \
\title TEXT NOT NULL, \
\epurl TEXT NOT NULL, \
\enctype TEXT NOT NULL,\
\status TEXT NOT NULL,\
\UNIQUE(castid, epurl),\
\UNIQUE(castid, episodeid))" [] >> return ())
setSchemaVer dbh 1
commit dbh
upgradeSchema dbh 1 tables
upgradeSchema dbh sv _ =
fail $ "Unrecognized DB schema version " ++ (show sv) ++
"; you probably need a newer hpodder to read this database."
setSchemaVer :: Connection -> Integer -> IO ()
setSchemaVer dbh sv =
do dbdebug $ "Setting schema version to " ++ show sv
run dbh "DELETE FROM schemaver" []
run dbh "INSERT INTO schemaver VALUES(?)" [toSql sv]
return ()
{- | Adds a new podcast to the database. Ignores the castid on the incoming
podcast, and returns a new object with the castid populated.
A duplicate add is an error. -}
addPodcast :: Connection -> Podcast -> IO Podcast
addPodcast dbh podcast =
do handleSql
(\e -> fail $ "Error adding podcast; perhaps this URL already exists\n"
++ show e) $
run dbh "INSERT INTO podcasts (castname, feedurl, pcenabled, lastupdate, lastattempt, failedattempts) VALUES (?, ?, ?, ?, ?, ?)"
[toSql (castname podcast), toSql (feedurl podcast),
toSql (fromEnum (pcenabled podcast)),
toSql (lastupdate podcast),
toSql (lastattempt podcast),
toSql (failedattempts podcast)]
r <- quickQuery dbh "SELECT castid FROM podcasts WHERE feedurl = ?"
[toSql (feedurl podcast)]
case r of
[[x]] -> return $ podcast {castid = fromSql x}
y -> fail $ "Unexpected result: " ++ show y
updatePodcast :: Connection -> Podcast -> IO ()
updatePodcast dbh podcast =
run dbh "UPDATE podcasts SET castname = ?, feedurl = ?, pcenabled = ?, \
\lastupdate = ?, lastattempt = ?, failedattempts = ? \
\WHERE castid = ?"
[toSql (castname podcast), toSql (feedurl podcast),
toSql (fromEnum (pcenabled podcast)),
toSql (lastupdate podcast),
toSql (lastattempt podcast),
toSql (failedattempts podcast), toSql (castid podcast)] >> return ()
{- | Remove a podcast. -}
removePodcast :: Connection -> Podcast -> IO ()
removePodcast dbh podcast =
do run dbh "DELETE FROM episodes WHERE castid = ?" [toSql (castid podcast)]
run dbh "DELETE FROM podcasts WHERE castid = ?" [toSql (castid podcast)]
return ()
getPodcasts :: Connection -> IO [Podcast]
getPodcasts dbh =
do res <- quickQuery dbh "SELECT castid, castname, feedurl, pcenabled,\
\lastupdate, lastattempt, failedattempts \
\FROM podcasts ORDER BY castid" []
return $ map podcast_convrow res
getPodcast :: Connection -> Integer -> IO [Podcast]
getPodcast dbh wantedid =
do res <- quickQuery dbh "SELECT castid, castname, feedurl, pcenabled,\
\lastupdate, lastattempt, failedattempts \
\FROM podcasts WHERE castid = ? ORDER BY castid" [toSql wantedid]
return $ map podcast_convrow res
getEpisodes :: Connection -> Podcast -> IO [Episode]
getEpisodes dbh pc =
do r <- quickQuery dbh "SELECT episodeid, title, epurl, enctype,\
\status, eplength, epfirstattempt, eplastattempt, \
\epfailedattempts, epguid FROM episodes \
\WHERE castid = ? ORDER BY \
\episodeid" [toSql (castid pc)]
return $ map toItem r
where toItem [sepid, stitle, sepurl, senctype, sstatus, slength,
slu, sla, sfa, sepguid] =
Episode {podcast = pc, epid = fromSql sepid,
eptitle = fromSql stitle,
epurl = fromSql sepurl, eptype = fromSql senctype,
epstatus = read (fromSql sstatus),
eplength = fromSql slength,
epfirstattempt = fromSql slu,
eplastattempt = fromSql sla,
epfailedattempts = fromSql sfa,
epguid = fromSql sepguid}
toItem x = error $ "Unexpected result in getEpisodes: " ++ show x
podcast_convrow [svid, svname, svurl, isenabled, lupdate, lattempt,
fattempts] =
Podcast {castid = fromSql svid, castname = fromSql svname,
feedurl = fromSql svurl, pcenabled = toEnum . fromSql $ isenabled,
lastupdate = fromSql lupdate, lastattempt = fromSql lattempt,
failedattempts = fromSql fattempts}
{- | Add a new episode. If the episode already exists, based solely on
looking at the GUID (if present), update the URL and title fields while
preserving other fields as they are. Returns the number of inserted rows. -}
addEpisode :: Connection -> Episode -> IO Integer
addEpisode dbh ep =
do
-- We have to be careful of cases where a feed may have two
-- different episodes with different GUIDs but identical URLs.
-- So if we have a GUID match here, we must have a conflict on URL,
-- so we ignore the request to change it.
when (epguid ep /= Nothing) $
do run dbh "UPDATE OR IGNORE episodes SET epurl = ?, epguid = ?, title = ? \
\WHERE castid = ? AND epguid = ?"
[toSql (epurl ep), toSql (epguid ep), toSql (eptitle ep),
toSql (castid (podcast ep)), toSql (epguid ep)]
return ()
-- if the UPDATE was successful, that means that something with the same
-- URL or GUID already exists, so the INSERT below will be ignored.
dbdebug "update done"
nextepid <- getepid
dbdebug $ "addEpisode: epid: " ++ show nextepid
dbdebug "addEpisode: running insertEpisode"
insertEpisode "INSERT OR IGNORE" dbh ep nextepid
where getepid =
do r <- quickQuery dbh "SELECT MAX(episodeid) FROM episodes WHERE castid = ?" [toSql (castid (podcast ep))]
case r of
[[SqlNull]] -> return 1
[[x]] -> return ((fromSql x) + (1::Int))
_ -> fail "Unexpected response in getepid"
{- | Update an episode. If it doesn't already exist, create it. -}
updateEpisode :: Connection -> Episode -> IO Integer
updateEpisode dbh ep = insertEpisode "INSERT OR REPLACE" dbh ep (epid ep)
insertEpisode insertsql dbh episode newepid =
run dbh (insertsql ++ " INTO episodes (castid, episodeid, title,\
\epurl, enctype, status, eplength, epfirstattempt, eplastattempt,\
\epfailedattempts, epguid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
[toSql (castid (podcast episode)), toSql newepid,
toSql (eptitle episode), toSql (epurl episode),
toSql (eptype episode), toSql (show (epstatus episode)),
toSql (eplength episode), toSql (epfirstattempt episode),
toSql (eplastattempt episode), toSql (epfailedattempts episode),
toSql (epguid episode)]
getSelectedPodcasts dbh [] = getSelectedPodcasts dbh ["all"]
getSelectedPodcasts dbh ["all"] = getPodcasts dbh
getSelectedPodcasts dbh podcastlist =
do r <- mapM (getPodcast dbh) (map read podcastlist)
return $ uniq $ concat r
getSelectedEpisodes :: Connection -> Podcast -> [String] -> IO [Episode]
getSelectedEpisodes _ _ [] = return []
getSelectedEpisodes dbh pc ["all"] = getEpisodes dbh pc
getSelectedEpisodes dbh pc episodelist =
do eps <- getEpisodes dbh pc
return $ uniq . filter (\e -> (epid e `elem` eplist)) $ eps
where eplist = map read episodelist
|