-- | -- Copyright : (c) Sam Truzjan 2013 -- License : BSD3 -- Maintainer : pxqr.sta@gmail.com -- Stability : experimental -- Portability : portable -- -- Parsing and rendering of magnet URIs. -- -- For more info see: -- -- -- Bittorrent specific info: -- -- {-# LANGUAGE NamedFieldPuns #-} module Data.Torrent.Magnet ( -- * Magnet Magnet(..) -- * Construction , nullMagnet , simpleMagnet , detailedMagnet -- * Conversion , parseMagnet , renderMagnet -- ** Extra , fromURI , toURI ) where import Control.Applicative import Control.Monad import Data.Map as M import Data.Maybe import Data.List as L import Data.URLEncoded as URL import Data.String import Data.Text as T import Data.Text.Encoding as T import Network.URI import Text.Read import Text.PrettyPrint as PP import Text.PrettyPrint.Class import Data.Torrent import Data.Torrent.InfoHash import Data.Torrent.Layout {----------------------------------------------------------------------- -- URN -----------------------------------------------------------------------} type NamespaceId = [Text] btih :: NamespaceId btih = ["btih"] -- | Uniform Resource Name - location-independent, resource -- identifier. data URN = URN { urnNamespace :: NamespaceId , urnString :: Text } deriving (Eq, Ord) instance Show URN where showsPrec n = showsPrec n . T.unpack . renderURN instance IsString URN where fromString = fromMaybe def . parseURN . T.pack where def = error "unable to parse URN" instance URLShow URN where urlShow = T.unpack . renderURN parseURN :: Text -> Maybe URN parseURN str = case T.split (== ':') str of uriScheme : body | T.toLower uriScheme == "urn" -> mkURN body | otherwise -> Nothing [] -> Nothing where mkURN [] = Nothing mkURN xs = Just $ URN { urnNamespace = L.init xs , urnString = L.last xs } renderURN :: URN -> Text renderURN URN {..} = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] urnToInfoHash :: URN -> Maybe InfoHash urnToInfoHash (URN {..}) | urnNamespace /= btih = Nothing | otherwise = textToInfoHash urnString infoHashToURN :: InfoHash -> URN infoHashToURN = URN btih . T.pack . show {----------------------------------------------------------------------- -- Magnet -----------------------------------------------------------------------} -- TODO multiple exact topics -- TODO supplement -- | An URI used to identify torrent. data Magnet = Magnet { -- | Resource hash. exactTopic :: !InfoHash -- | Might be used to display name while waiting for metadata. , displayName :: Maybe Text -- | Size of the resource in bytes. , exactLength :: Maybe Integer , manifest :: Maybe String -- | Search string. , keywordTopic :: Maybe String , acceptableSource :: Maybe URI , exactSource :: Maybe URI , tracker :: Maybe URI , supplement :: Map Text Text } deriving (Eq, Ord) instance Show Magnet where show = renderMagnet {-# INLINE show #-} instance Read Magnet where readsPrec _ xs | Just m <- parseMagnet mstr = [(m, rest)] | otherwise = [] where (mstr, rest) = L.break (== ' ') xs instance IsString Magnet where fromString = fromMaybe def . parseMagnet where def = error "unable to parse magnet" instance URLEncode Magnet where urlEncode = toQuery {-# INLINE urlEncode #-} instance Pretty Magnet where pretty = PP.text . renderMagnet -- | Set exact topic only, other params are empty. nullMagnet :: InfoHash -> Magnet nullMagnet u = Magnet { exactTopic = u , displayName = Nothing , exactLength = Nothing , manifest = Nothing , keywordTopic = Nothing , acceptableSource = Nothing , exactSource = Nothing , tracker = Nothing , supplement = M.empty } -- | A simple magnet link including infohash ('xt' param) and display -- name ('dn' param). -- simpleMagnet :: Torrent -> Magnet simpleMagnet Torrent {tInfoDict = InfoDict {..}} = (nullMagnet idInfoHash) { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo } -- | Like 'simpleMagnet' but also include exactLength ('xl' param) and -- tracker ('tr' param). detailedMagnet :: Torrent -> Magnet detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} = (simpleMagnet t) { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo , tracker = Just tAnnounce } fromQuery :: URLEncoded -> Either String Magnet fromQuery q | Just urnStr <- URL.lookup ("xt" :: String) q , Just urn <- parseURN $ T.pack urnStr , Just infoHash <- urnToInfoHash urn = return $ Magnet { exactTopic = infoHash , displayName = T.pack <$> URL.lookup ("dn" :: String) q , exactLength = readMaybe =<< URL.lookup ("xl" :: String) q , manifest = URL.lookup ("mt" :: String) q , keywordTopic = URL.lookup ("kt" :: String) q , acceptableSource = parseURI =<< URL.lookup ("as" :: String) q , exactSource = parseURI =<< URL.lookup ("xs" :: String) q , tracker = parseURI =<< URL.lookup ("tr" :: String) q , supplement = M.empty } | otherwise = Left "exact topic not defined" toQuery :: Magnet -> URLEncoded toQuery Magnet {..} = s "xt" %= infoHashToURN exactTopic %& s "dn" %=? (T.unpack <$> displayName) %& s "xl" %=? exactLength %& s "mt" %=? manifest %& s "kt" %=? keywordTopic %& s "as" %=? acceptableSource %& s "xs" %=? exactSource %& s "tr" %=? tracker where s :: String -> String; s = id magnetScheme :: URI magnetScheme = URI { uriScheme = "magnet:" , uriAuthority = Nothing , uriPath = "" , uriQuery = "" , uriFragment = "" } isMagnetURI :: URI -> Bool isMagnetURI u = u { uriQuery = "" } == magnetScheme -- | The same as 'parseMagnet' but useful if you alread have a parsed -- uri. fromURI :: URI -> Either String Magnet fromURI u @ URI {..} | not (isMagnetURI u) = Left "this is not a magnet link" | otherwise = importURI u >>= fromQuery -- | The same as 'renderMagnet' but useful if you need an uri. toURI :: Magnet -> URI toURI m = magnetScheme %? urlEncode m etom :: Either a b -> Maybe b etom = either (const Nothing) Just -- | Try to parse magnet link from urlencoded string. parseMagnet :: String -> Maybe Magnet parseMagnet = parseURI >=> etom . fromURI -- | Render magnet link to urlencoded string renderMagnet :: Magnet -> String renderMagnet = show . toURI