{-# LANGUAGE OverloadedStrings #-} module Main where import Database.MongoDB as DB import Text.HTML.TagSoup import System.Environment import System.IO import System.Process import Text.Printf import System.Exit import Control.Monad.Trans import Network.URI import Data.List.Split import Data.Maybe import Data.List (isPrefixOf,nub) import Data.Char import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BC8 import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Map as Map import Control.Monad import Control.Exception (evaluate) import Prelude import Data.Classify.Television import Epguides main :: IO () main = do args <- getArgs let addr = "localhost" case args of [port, "magnet"] -> withMeteor addr port run [port, "shows"] -> withMeteor addr port (getTVShows >>= liftIO . mapM_ putStrLn) [port, "add", name] -> withMeteor addr port (addTVShow name) [port, "remove", name] -> withMeteor addr port (removeTVShow name) [port, "epguides", name] -> withMeteor addr port (mkEpguidesEntries name) [port, "epguides_all"] -> withMeteor addr port (mkAllEpguidesEntries) [port, "set_epguides", name, url] -> withMeteor addr port (setEpguides name url) [port, "rebuild"] -> withMeteor addr port rebuild _ -> printUsage withMeteor addr port fn = do pipe <- runIOE $ connect (Host addr (PortNumber (fromIntegral (read port :: Integer)))) access pipe slaveOk "meteor" fn close pipe printUsage :: IO () printUsage = do prog <- getProgName hPutStrLn stderr $ printf "Usage: %s \n" prog hPutStrLn stderr $ printf "Commands:" hPutStrLn stderr $ printf " magnet Fetch magnet links from stdin and add to DB." hPutStrLn stderr $ printf " shows List the recognized TV shows." hPutStrLn stderr $ printf " add Add a name to the list of known TV shows." hPutStrLn stderr $ printf " remove Remove a name to the list of known TV shows." hPutStrLn stderr $ printf " epguides Fetch titles and air-times from epguides." hPutStrLn stderr $ printf " epguides_all Fetch titles and air-times from epguides." hPutStrLn stderr $ printf " set_epguides Associate with ." hPutStrLn stderr $ printf " rebuild Recalculate computed properties in the DB." exitWith ExitSuccess isMagnetLink :: URI -> Bool isMagnetLink uri = uriScheme uri == "magnet:" getDownloadName :: URI -> String getDownloadName uri = fromMaybe "" $ listToMaybe $ [ dn | ["dn", dn] <- map (splitOn "=") $ splitOn "&" (dropWhile (== '?') $ uriQuery uri) ] getHash :: URI -> String getHash uri = fromMaybe "" $ listToMaybe $ [ xt | ["xt", xt] <- map (splitOn "=") $ splitOn "&" (dropWhile (== '?') $ uriQuery uri) ] getTVShows = do records <- rest =<< find (select [] "meta") {project = [ "name" =: (1::Integer) ]} return $ nub $ mapMaybe (DB.lookup "name") records addTVShow name = insert "meta" ["name" =: name] setEpguides name url = do shows <- getTVShows unless (name `elem` shows) $ error $ "Unknown TV series: " ++ name modify (select ["name" =: name] "meta") ["$set" =: ["epguides" =: url]] getEpguidesURL name = do entry <- findOne (select ["name" =: name] "meta") case DB.lookup "epguides" =<< entry of Nothing -> error $ "No epguides URL associated with " ++ show name Just url -> return url removeTVShow name = delete (select ["name" =: name] "meta") getTags :: String -> [String] getTags = words . lower where lower str = [ if isAlphaNum c then c else ' ' | c <- str ] run = do inp <- liftIO $ getContents let hrefs = [ href | TagOpen "a" attrs <- parseTags inp, ("href", href) <- attrs ] shows <- getTVShows insertMagnetLinks shows hrefs insertMagnetLinks shows uris = do let magnets = [ uri | Just uri <- map parseURI uris, isMagnetLink uri ] elts = [ (uri, elt) | uri <- magnets , let dn = getDownloadName uri , Just elt <- [tryClassify shows dn] ] forM_ elts $ \(uri, (name, version, title)) -> do liftIO $ putStrLn $ "Adding element: " ++ show (name, version, title) let query = [ "name" =: name , case version of Version season episode -> "episode" =: [ "season" =: season, "episode" =: episode ] DateVersion year month day -> "airdate" =: [ "year" =: year, "month" =: month, "day" =: day ] ] inst = [ "link" =: show uri, "hash" =: getHash uri, "tags" =: getTags title ] modify (select query "episodes") ["$addToSet" =: ["instances" =: inst]] return () rebuild = do shows <- getTVShows records <- rest =<< find (select ["instances" =: ["$exists" =: True]] "episodes") forM_ records $ \record -> do let record_id = DB.lookup "_id" record :: Maybe ObjectId forM_ (fromMaybe [] (DB.lookup "instances" record)) $ \inst -> case DB.lookup "link" inst of Nothing -> return () Just uri -> do modify (select ["_id" =: record_id] "episodes") ["$pull" =: ["instances" =: inst]] insertMagnetLinks shows [uri] mkAllEpguidesEntries = do shows <- getTVShows mapM_ mkEpguidesEntries shows hGetContentsSafe :: Handle -> IO String hGetContentsSafe handle = do inp <- BS.hGetContents handle case T.decodeUtf8' inp of Right txt -> return $ T.unpack txt Left err -> do putStrLn $ "Failed to decode as UTF8: " ++ show err return $ BC8.unpack inp {- {name: TV show ,searchable: string ,airdate: {year: int, month: int, day: int} ,episode: {season: int, episode: int} ,title: string ,instances: [{link: magnet, hash: hash, tags: [string]}] } -} mkEpguidesEntries name = do shows <- getTVShows unless (name `elem` shows) $ error $ "Unknown TV series: " ++ name liftIO $ putStrLn $ "Fetching meta information for show: " ++ name epguides_url <- getEpguidesURL name (inh, outh, errh, pid) <- liftIO $ runInteractiveProcess "wget" ["-q", "-O", "-", epguides_url] Nothing Nothing liftIO $ hClose inh >> hClose errh inp <- liftIO $ hGetContentsSafe outh liftIO $ evaluate (length inp) _ <- liftIO $ waitForProcess pid db <- rest =<< find (select ["name" =: name] "episodes") { project = ["episode" =: True, "title" =: True] } let dbAssoc = catMaybes [ do idx <- DB.lookup "episode" x title <- DB.lookup "title" x return (idx, title) | x <- db ] forM_ (parseEpguide inp) $ \(EpIdx season episode, (year, month, day), title) -> do let query = ["name" =: name, "episode" =: idx ] idx = ["season" =: season, "episode" =: episode ] airdate = ["year" =: year, "month" =: month, "day" =: day] case Prelude.lookup idx dbAssoc of Nothing -> do liftIO $ putStrLn $ "Inserting new: " ++ show (season, episode, year, month, day, title) insert_ "episodes" [ "name" =: name , "episode" =: ["season" =: season, "episode" =: episode] , "airdate" =: airdate , "title" =: title ] Just oldTitle | oldTitle == title -> --liftIO $ putStrLn $ "Skipping: " ++ show (season, episode, year, month, day, title) return () Just oldTitle -> do liftIO $ putStrLn $ "Updating: " ++ show (season, episode, year, month, day, oldTitle, title) modify (select query "episodes") ["$set" =: ["title" =: title, "airdate" =: airdate]]