|
| 1 | +{-# LANGUAGE DeriveGeneric #-} |
| 2 | +{-# LANGUAGE OverloadedStrings #-} |
| 3 | +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} |
| 4 | +{-# OPTIONS_GHC -Wno-name-shadowing #-} |
| 5 | +{-# LANGUAGE FlexibleInstances #-} |
| 6 | +{-# LANGUAGE MultiParamTypeClasses #-} |
| 7 | + |
| 8 | +module Main where |
| 9 | + |
| 10 | +-- Standard library imports |
| 11 | +import System.Environment (getArgs, getProgName, lookupEnv) |
| 12 | + |
| 13 | +-- Third-party library imports |
| 14 | +import Control.Lens (Identity (runIdentity), (^?)) |
| 15 | +import Data.Aeson (FromJSON (parseJSON), ToJSON, |
| 16 | + Value (Object), decodeStrict, (.:)) |
| 17 | +import Data.Aeson.Lens (AsNumber (_Integer), key, nth) |
| 18 | +import Data.ByteString (ByteString) |
| 19 | +import qualified Data.ByteString.Char8 as BS |
| 20 | +import Data.List as L (intercalate) |
| 21 | +import Data.Text as T (unpack) |
| 22 | +import GHC.Generics (Generic) |
| 23 | +import Network.HTTP.Simple (Query, getResponseBody, httpBS, |
| 24 | + parseRequest_, setRequestHeader, |
| 25 | + setRequestQueryString) |
| 26 | +import System.FilePath (takeDirectory) |
| 27 | +import Text.Ginger (IncludeResolver, SourcePos, Template, |
| 28 | + ToGVal (..), dict, easyRender, |
| 29 | + parseGinger) |
| 30 | +import Control.Exception (try, SomeException) |
| 31 | + |
| 32 | +-- Data type definitions |
| 33 | +data MainRelease = MainRelease { |
| 34 | + artists :: [String], |
| 35 | + title :: String, |
| 36 | + year :: Int, |
| 37 | + released :: String, |
| 38 | + imageUrl :: String, |
| 39 | + labels :: [String], |
| 40 | + uri :: String |
| 41 | +} deriving (Show, Eq, Generic) |
| 42 | + |
| 43 | +instance ToJSON MainRelease |
| 44 | + |
| 45 | +instance ToGVal m MainRelease where |
| 46 | + toGVal release = dict [ |
| 47 | + ("artists", toGVal . L.intercalate ", " . artists $ release), |
| 48 | + ("title", toGVal $ title release), |
| 49 | + ("year", toGVal $ year release), |
| 50 | + ("released", toGVal $ released release), |
| 51 | + ("imageUrl", toGVal $ imageUrl release), |
| 52 | + ("labels", toGVal . L.intercalate ", " . labels $ release), |
| 53 | + ("uri", toGVal $ uri release) |
| 54 | + ] |
| 55 | + |
| 56 | + |
| 57 | +instance FromJSON MainRelease where |
| 58 | + parseJSON (Object v) = do |
| 59 | + artists <- v .: "artists" >>= traverse (.: "name") |
| 60 | + title <- v .: "title" |
| 61 | + year <- v .: "year" |
| 62 | + released <- v .: "released" |
| 63 | + images <- v .: "images" |
| 64 | + imageUrl <- case images of |
| 65 | + (img:_) -> img .: "resource_url" |
| 66 | + [] -> fail "No images found" |
| 67 | + labels <- v .: "labels" >>= traverse (.: "name") |
| 68 | + uri <- v .: "uri" |
| 69 | + return MainRelease { |
| 70 | + artists = artists, |
| 71 | + title = title, |
| 72 | + year = year, |
| 73 | + released = released, |
| 74 | + imageUrl = imageUrl, |
| 75 | + labels = labels, |
| 76 | + uri = uri |
| 77 | + } |
| 78 | + |
| 79 | +-- Helper functions |
| 80 | +runDiscogsQuery :: Query -> String -> IO ByteString |
| 81 | +runDiscogsQuery query url = do |
| 82 | + maybeKey <- lookupEnv "DISCOG_KEY" |
| 83 | + maybeSecret <- lookupEnv "DISCOG_SECRET" |
| 84 | + (key, secret) <- case (maybeKey, maybeSecret) of |
| 85 | + (Just k, Just s) -> return (k, s) |
| 86 | + _ -> error "Environment variables DISCOG_KEY and/or DISCOG_SECRET are not set" |
| 87 | + let request = |
| 88 | + setRequestQueryString query $ |
| 89 | + setRequestHeader "Authorization" [BS.pack $ "Discogs key=" ++ key ++ ", secret=" ++ secret] $ |
| 90 | + setRequestHeader "User-Agent" [ "pull-album-info/1.0 ([email protected])"] $ |
| 91 | + parseRequest_ url |
| 92 | + getResponseBody <$> httpBS request |
| 93 | + |
| 94 | +getMasterReleaseId :: String -> String -> IO String |
| 95 | +getMasterReleaseId artistName albumName = do |
| 96 | + let url = "https://api.discogs.com/database/search" |
| 97 | + query = |
| 98 | + [ ("artist", Just $ BS.pack artistName), |
| 99 | + ("release_title", Just $ BS.pack albumName), |
| 100 | + ("type", Just "master") |
| 101 | + ] |
| 102 | + body <- BS.unpack <$> runDiscogsQuery query url |
| 103 | + case body ^? key "results" . nth 0 . key "master_id" . _Integer of |
| 104 | + Just masterId -> return $ show masterId |
| 105 | + Nothing -> fail "Failed to extract master_id from the response" |
| 106 | + |
| 107 | +getMainReleaseId :: String -> IO String |
| 108 | +getMainReleaseId masterId = do |
| 109 | + let url = "https://api.discogs.com/masters/" ++ masterId |
| 110 | + body <- BS.unpack <$> runDiscogsQuery [] url |
| 111 | + case body ^? key "main_release" . _Integer of |
| 112 | + Just mainId -> return $ show mainId |
| 113 | + Nothing -> fail "Failed to extract main_release from the response" |
| 114 | + |
| 115 | +getMainRelease :: String -> IO MainRelease |
| 116 | +getMainRelease releaseId = do |
| 117 | + let url = "https://api.discogs.com/releases/" ++ releaseId |
| 118 | + body <- runDiscogsQuery [] url |
| 119 | + case (decodeStrict body :: Maybe MainRelease) of |
| 120 | + Just release -> return release |
| 121 | + Nothing -> fail "Cannot decode main release" |
| 122 | + |
| 123 | +nullResolver :: IncludeResolver Identity |
| 124 | +nullResolver = const $ return Nothing |
| 125 | + |
| 126 | +getTemplate :: String -> Template SourcePos |
| 127 | +getTemplate content = either (error . show) id . runIdentity $ |
| 128 | + parseGinger nullResolver Nothing content |
| 129 | + |
| 130 | +templatePath :: IO String |
| 131 | +templatePath = do |
| 132 | + progName <- getProgName |
| 133 | + return $ takeDirectory progName ++ "/app/templates/post.md" |
| 134 | + |
| 135 | +runGenAlbumPost :: String -> String -> IO String |
| 136 | +runGenAlbumPost artistName albumName = do |
| 137 | + -- Get the MainRelease of the album |
| 138 | + release <- getMasterReleaseId artistName albumName |
| 139 | + >>= getMainReleaseId |
| 140 | + >>= getMainRelease |
| 141 | + content <- templatePath >>= readFile |
| 142 | + return $ T.unpack . easyRender release $ getTemplate content |
| 143 | + |
| 144 | +-- Main function |
| 145 | +main :: IO () |
| 146 | +main = do |
| 147 | + args <- getArgs |
| 148 | + case args of |
| 149 | + [artistName, albumName, branchName] -> do |
| 150 | + result <- try $ runGenAlbumPost artistName albumName :: IO (Either SomeException String) |
| 151 | + post <- case result of |
| 152 | + Left _ -> do |
| 153 | + _ <- putStrLn "Cannot get album info from Discog, falling back to default post template" |
| 154 | + templatePath >>= readFile |
| 155 | + Right output -> return output |
| 156 | + writeFile branchName post |
| 157 | + putStrLn "done" |
| 158 | + _ -> putStrLn "Usage: pull_album_info <artist_name> <album_name> <branch_name>" |
0 commit comments