feat(users/Profpatsch/whatcd-resolver): allow more than albums
When searching for an artist, we only put albums before, but now let’s fetch all releases and group them by release type. I group & sort on the backend instead of the database, cause it’s a lot easier to express in Haskell code and the amount of data stays the same (except for the filter for singles which I might move into the SQL at one point?) Adds a prelude module for better dealing with comparisons. Change-Id: Iff235af611b6e1bac71b118a8a04fc73cacd169f Reviewed-on: https://cl.tvl.fyi/c/depot/+/12951 Reviewed-by: Profpatsch <mail@profpatsch.de> Tested-by: BuildkiteCI
This commit is contained in:
parent
c7fa114295
commit
0319b5e6c0
5 changed files with 127 additions and 11 deletions
|
|
@ -7,6 +7,7 @@ pkgs.haskellPackages.mkDerivation {
|
||||||
src = depot.users.Profpatsch.exactSource ./. [
|
src = depot.users.Profpatsch.exactSource ./. [
|
||||||
./my-prelude.cabal
|
./my-prelude.cabal
|
||||||
./src/Aeson.hs
|
./src/Aeson.hs
|
||||||
|
./src/Comparison.hs
|
||||||
./src/Json.hs
|
./src/Json.hs
|
||||||
./src/Json/Enc.hs
|
./src/Json/Enc.hs
|
||||||
./src/Arg.hs
|
./src/Arg.hs
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ library
|
||||||
Aeson
|
Aeson
|
||||||
Arg
|
Arg
|
||||||
AtLeast
|
AtLeast
|
||||||
|
Comparison
|
||||||
Json
|
Json
|
||||||
Json.Enc
|
Json.Enc
|
||||||
Test
|
Test
|
||||||
|
|
|
||||||
61
users/Profpatsch/my-prelude/src/Comparison.hs
Normal file
61
users/Profpatsch/my-prelude/src/Comparison.hs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
-- | Helper module for working better with 'Comparison' contravariants.
|
||||||
|
module Comparison
|
||||||
|
( module Data.Functor.Contravariant,
|
||||||
|
Comparison (..),
|
||||||
|
defaultComparison1,
|
||||||
|
down,
|
||||||
|
down1,
|
||||||
|
maybeDown,
|
||||||
|
groupAllWithComparison,
|
||||||
|
listIndexComparison,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Functor.Classes
|
||||||
|
import Data.Functor.Contravariant
|
||||||
|
import Data.List qualified as List
|
||||||
|
import Data.List.NonEmpty
|
||||||
|
import Data.List.NonEmpty qualified as NonEmpty
|
||||||
|
|
||||||
|
-- | Unlift an Ord1 to a `Comparison` function. Analogous to `defaultComparison`.
|
||||||
|
defaultComparison1 :: (Ord1 f) => Comparison a -> Comparison (f a)
|
||||||
|
defaultComparison1 = Comparison . liftCompare . getComparison
|
||||||
|
|
||||||
|
-- | Invert the ordering of the comparison
|
||||||
|
down :: Comparison a -> Comparison a
|
||||||
|
down c = Comparison $ \a b -> case getComparison c a b of
|
||||||
|
LT -> GT
|
||||||
|
EQ -> EQ
|
||||||
|
GT -> LT
|
||||||
|
|
||||||
|
-- | Invert the ordering of the outer comparison function but not the inner one
|
||||||
|
--
|
||||||
|
-- Can be used to e.g. change the ordering of `Maybe` values, but not the ordering of the inner values.
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > maybeDown = down1 (defaultComparison1 @Maybe)
|
||||||
|
-- > maybeDown (Just 1) (Just 2) == LT
|
||||||
|
-- > maybeDown (Just 1) Nothing == LT
|
||||||
|
-- > maybeDown Nothing (Just 2) == GT
|
||||||
|
down1 :: (Comparison a -> Comparison b) -> Comparison a -> Comparison b
|
||||||
|
down1 inner = down . inner . down
|
||||||
|
|
||||||
|
-- | Sort every Nothing behind every Just
|
||||||
|
maybeDown :: Comparison a -> Comparison (Maybe a)
|
||||||
|
maybeDown = down1 (defaultComparison1 @Maybe)
|
||||||
|
|
||||||
|
-- | Group all items in the list that are equal according to the comparison function. Sort the list first so every equivalence class has at most one list in the output.
|
||||||
|
groupAllWithComparison :: Comparison a -> [a] -> [NonEmpty a]
|
||||||
|
groupAllWithComparison c xs =
|
||||||
|
NonEmpty.groupBy
|
||||||
|
(getEquivalence $ comparisonEquivalence c)
|
||||||
|
$ List.sortBy (getComparison c) xs
|
||||||
|
|
||||||
|
-- | Everything in the list is ordered in the order of equality in the list.
|
||||||
|
--
|
||||||
|
-- Any item is not in the list is automatically GT to every item in the list,
|
||||||
|
-- and EQ to every item not in the list.
|
||||||
|
listIndexComparison :: (Eq a) => [a] -> Comparison a
|
||||||
|
listIndexComparison xs =
|
||||||
|
(\x -> List.elemIndex x xs)
|
||||||
|
>$< maybeDown (defaultComparison @Int)
|
||||||
|
|
@ -4,6 +4,7 @@ module Redacted where
|
||||||
|
|
||||||
import AppT
|
import AppT
|
||||||
import Arg
|
import Arg
|
||||||
|
import Comparison
|
||||||
import Control.Monad.Logger.CallStack
|
import Control.Monad.Logger.CallStack
|
||||||
import Control.Monad.Reader
|
import Control.Monad.Reader
|
||||||
import Data.Aeson qualified as Json
|
import Data.Aeson qualified as Json
|
||||||
|
|
@ -365,6 +366,7 @@ assertOneUpdated span name x = case x.numberOfRowsAffected of
|
||||||
data TorrentData transmissionInfo = TorrentData
|
data TorrentData transmissionInfo = TorrentData
|
||||||
{ groupId :: Int,
|
{ groupId :: Int,
|
||||||
torrentId :: Int,
|
torrentId :: Int,
|
||||||
|
releaseType :: ReleaseType,
|
||||||
seedingWeight :: Int,
|
seedingWeight :: Int,
|
||||||
artists :: [T2 "artistId" Int "artistName" Text],
|
artists :: [T2 "artistId" Int "artistName" Text],
|
||||||
torrentGroupJson :: TorrentGroupJson,
|
torrentGroupJson :: TorrentGroupJson,
|
||||||
|
|
@ -372,6 +374,34 @@ data TorrentData transmissionInfo = TorrentData
|
||||||
torrentFormat :: Text
|
torrentFormat :: Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | https://redacted.sh/wiki.php?action=article&id=455#_1804298149
|
||||||
|
newtype ReleaseType = ReleaseType {unReleaseType :: Text}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
releaseTypeComparison :: Comparison ReleaseType
|
||||||
|
releaseTypeComparison = listIndexComparison [releaseTypeAlbum, releaseTypeSoundtrack, releaseTypeEP, releaseTypeAnthology, releaseTypeCompilation, releaseTypeSingle, releaseTypeLiveAlbum, releaseTypeRemix, releaseTypeBootleg, releaseTypeInterview, releaseTypeMixtape, releaseTypeDemo, releaseTypeConcertRecording, releaseTypeDJMix, releaseTypeUnknown, releaseTypeProducedBy, releaseTypeComposition, releaseTypeRemixedBy, releaseTypeGuestAppearance]
|
||||||
|
|
||||||
|
releaseTypeAlbum, releaseTypeSoundtrack, releaseTypeEP, releaseTypeAnthology, releaseTypeCompilation, releaseTypeSingle, releaseTypeLiveAlbum, releaseTypeRemix, releaseTypeBootleg, releaseTypeInterview, releaseTypeMixtape, releaseTypeDemo, releaseTypeConcertRecording, releaseTypeDJMix, releaseTypeUnknown, releaseTypeProducedBy, releaseTypeComposition, releaseTypeRemixedBy, releaseTypeGuestAppearance :: ReleaseType
|
||||||
|
releaseTypeAlbum = ReleaseType "Album"
|
||||||
|
releaseTypeSoundtrack = ReleaseType "Soundtrack"
|
||||||
|
releaseTypeEP = ReleaseType "EP"
|
||||||
|
releaseTypeAnthology = ReleaseType "Anthology"
|
||||||
|
releaseTypeCompilation = ReleaseType "Compilation"
|
||||||
|
releaseTypeSingle = ReleaseType "Single"
|
||||||
|
releaseTypeLiveAlbum = ReleaseType "Live album"
|
||||||
|
releaseTypeRemix = ReleaseType "Remix"
|
||||||
|
releaseTypeBootleg = ReleaseType "Bootleg"
|
||||||
|
releaseTypeInterview = ReleaseType "Interview"
|
||||||
|
releaseTypeMixtape = ReleaseType "Mixtape"
|
||||||
|
releaseTypeDemo = ReleaseType "Demo"
|
||||||
|
releaseTypeConcertRecording = ReleaseType "Concert Recording"
|
||||||
|
releaseTypeDJMix = ReleaseType "DJ Mix"
|
||||||
|
releaseTypeUnknown = ReleaseType "Unknown"
|
||||||
|
releaseTypeProducedBy = ReleaseType "Produced By"
|
||||||
|
releaseTypeComposition = ReleaseType "Composition"
|
||||||
|
releaseTypeRemixedBy = ReleaseType "Remixed By"
|
||||||
|
releaseTypeGuestAppearance = ReleaseType "Guest Appearance"
|
||||||
|
|
||||||
data TorrentGroupJson = TorrentGroupJson
|
data TorrentGroupJson = TorrentGroupJson
|
||||||
{ groupName :: Text,
|
{ groupName :: Text,
|
||||||
groupYear :: Natural
|
groupYear :: Natural
|
||||||
|
|
@ -427,6 +457,7 @@ getBestTorrents opts = do
|
||||||
tg.group_id,
|
tg.group_id,
|
||||||
t.torrent_id,
|
t.torrent_id,
|
||||||
t.seeding_weight,
|
t.seeding_weight,
|
||||||
|
tg.full_json_result->>'releaseType' AS release_type,
|
||||||
t.full_json_result->'artists' AS artists,
|
t.full_json_result->'artists' AS artists,
|
||||||
tg.full_json_result->>'groupName' AS group_name,
|
tg.full_json_result->>'groupName' AS group_name,
|
||||||
tg.full_json_result->>'groupYear' AS group_year,
|
tg.full_json_result->>'groupYear' AS group_year,
|
||||||
|
|
@ -451,6 +482,7 @@ getBestTorrents opts = do
|
||||||
groupId <- Dec.fromField @Int
|
groupId <- Dec.fromField @Int
|
||||||
torrentId <- Dec.fromField @Int
|
torrentId <- Dec.fromField @Int
|
||||||
seedingWeight <- Dec.fromField @Int
|
seedingWeight <- Dec.fromField @Int
|
||||||
|
releaseType <- ReleaseType <$> Dec.text
|
||||||
artists <- Dec.json $
|
artists <- Dec.json $
|
||||||
Json.eachInArray $ do
|
Json.eachInArray $ do
|
||||||
id_ <- Json.keyLabel @"artistId" "id" (Json.asIntegral @_ @Int)
|
id_ <- Json.keyLabel @"artistId" "id" (Json.asIntegral @_ @Int)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ module WhatcdResolver where
|
||||||
|
|
||||||
import AppT
|
import AppT
|
||||||
import Arg
|
import Arg
|
||||||
|
import Comparison
|
||||||
import Control.Category qualified as Cat
|
import Control.Category qualified as Cat
|
||||||
import Control.Monad.Catch.Pure (runCatch)
|
import Control.Monad.Catch.Pure (runCatch)
|
||||||
import Control.Monad.Logger.CallStack
|
import Control.Monad.Logger.CallStack
|
||||||
|
|
@ -15,6 +16,7 @@ import Data.Aeson.KeyMap qualified as KeyMap
|
||||||
import Data.Error.Tree
|
import Data.Error.Tree
|
||||||
import Data.HashMap.Strict qualified as HashMap
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
import Data.List qualified as List
|
import Data.List qualified as List
|
||||||
|
import Data.List.NonEmpty qualified as NonEmpty
|
||||||
import Data.Map.Strict qualified as Map
|
import Data.Map.Strict qualified as Map
|
||||||
import Data.Pool qualified as Pool
|
import Data.Pool qualified as Pool
|
||||||
import Data.Text qualified as Text
|
import Data.Text qualified as Text
|
||||||
|
|
@ -347,9 +349,11 @@ artistPage ::
|
||||||
dat ->
|
dat ->
|
||||||
m Html
|
m Html
|
||||||
artistPage dat = runTransaction $ do
|
artistPage dat = runTransaction $ do
|
||||||
fresh <- getBestTorrentsData (Just $ getLabel @"artistRedactedId" dat)
|
fresh <-
|
||||||
|
getBestTorrentsData
|
||||||
|
(Just $ getLabel @"artistRedactedId" dat)
|
||||||
let artistName = fresh & findMaybe (\t -> t.artists & findMaybe (\a -> if a.artistId == (dat.artistRedactedId & fromIntegral @Natural @Int) then Just a.artistName else Nothing))
|
let artistName = fresh & findMaybe (\t -> t.artists & findMaybe (\a -> if a.artistId == (dat.artistRedactedId & fromIntegral @Natural @Int) then Just a.artistName else Nothing))
|
||||||
let torrents = mkBestTorrentsTable fresh
|
let torrents = mkBestTorrentsTable (label @"groupByReleaseType" True) fresh
|
||||||
pure $
|
pure $
|
||||||
htmlPageChrome
|
htmlPageChrome
|
||||||
( case artistName of
|
( case artistName of
|
||||||
|
|
@ -522,8 +526,7 @@ snipsRedactedSearch ::
|
||||||
snipsRedactedSearch dat = do
|
snipsRedactedSearch dat = do
|
||||||
t <-
|
t <-
|
||||||
redactedSearchAndInsert
|
redactedSearchAndInsert
|
||||||
[ ("searchstr", dat.searchstr),
|
[ ("searchstr", dat.searchstr)
|
||||||
("releasetype", "album")
|
|
||||||
]
|
]
|
||||||
runTransaction $ do
|
runTransaction $ do
|
||||||
t
|
t
|
||||||
|
|
@ -544,7 +547,7 @@ getBestTorrentsTable ::
|
||||||
Transaction m Html
|
Transaction m Html
|
||||||
getBestTorrentsTable dat = do
|
getBestTorrentsTable dat = do
|
||||||
fresh <- getBestTorrentsData dat
|
fresh <- getBestTorrentsData dat
|
||||||
pure $ mkBestTorrentsTable fresh
|
pure $ mkBestTorrentsTable (label @"groupByReleaseType" False) fresh
|
||||||
|
|
||||||
doIfJust :: (Applicative f) => (a -> f ()) -> Maybe a -> f ()
|
doIfJust :: (Applicative f) => (a -> f ()) -> Maybe a -> f ()
|
||||||
doIfJust = traverse_
|
doIfJust = traverse_
|
||||||
|
|
@ -585,6 +588,7 @@ getBestTorrentsData artistFilter = inSpan' "get torrents table data" $ \span ->
|
||||||
else pure bestStale
|
else pure bestStale
|
||||||
pure $
|
pure $
|
||||||
bestBest
|
bestBest
|
||||||
|
& filter (\td -> td.releaseType /= releaseTypeCompilation)
|
||||||
-- we have to update the status of every torrent that’s not in tranmission anymore
|
-- we have to update the status of every torrent that’s not in tranmission anymore
|
||||||
-- TODO I feel like it’s easier (& more correct?) to just do the database request again …
|
-- TODO I feel like it’s easier (& more correct?) to just do the database request again …
|
||||||
<&> ( \td -> case td.torrentStatus of
|
<&> ( \td -> case td.torrentStatus of
|
||||||
|
|
@ -598,8 +602,8 @@ getBestTorrentsData artistFilter = inSpan' "get torrents table data" $ \span ->
|
||||||
NoTorrentFileYet -> td {torrentStatus = NoTorrentFileYet}
|
NoTorrentFileYet -> td {torrentStatus = NoTorrentFileYet}
|
||||||
)
|
)
|
||||||
|
|
||||||
mkBestTorrentsTable :: [TorrentData (Label "percentDone" Percentage)] -> Html
|
mkBestTorrentsTable :: Label "groupByReleaseType" Bool -> [TorrentData (Label "percentDone" Percentage)] -> Html
|
||||||
mkBestTorrentsTable fresh = do
|
mkBestTorrentsTable opts fresh = do
|
||||||
let localTorrent b = case b.torrentStatus of
|
let localTorrent b = case b.torrentStatus of
|
||||||
NoTorrentFileYet ->
|
NoTorrentFileYet ->
|
||||||
[hsx|
|
[hsx|
|
||||||
|
|
@ -614,8 +618,8 @@ mkBestTorrentsTable fresh = do
|
||||||
|]
|
|]
|
||||||
InTransmission info -> [hsx|{info.transmissionInfo.percentDone.unPercentage}% done|]
|
InTransmission info -> [hsx|{info.transmissionInfo.percentDone.unPercentage}% done|]
|
||||||
NotInTransmissionYet -> [hsx|<button hx-post="snips/redacted/startTorrentFile" hx-swap="outerHTML" hx-vals={Enc.encToBytesUtf8 $ Enc.object [("torrent-id", Enc.int b.torrentId)]}>Start Torrent</button>|]
|
NotInTransmissionYet -> [hsx|<button hx-post="snips/redacted/startTorrentFile" hx-swap="outerHTML" hx-vals={Enc.encToBytesUtf8 $ Enc.object [("torrent-id", Enc.int b.torrentId)]}>Start Torrent</button>|]
|
||||||
let bestRows =
|
let bestRows rowData =
|
||||||
fresh
|
rowData
|
||||||
& foldMap
|
& foldMap
|
||||||
( \b -> do
|
( \b -> do
|
||||||
let artists =
|
let artists =
|
||||||
|
|
@ -639,6 +643,7 @@ mkBestTorrentsTable fresh = do
|
||||||
{Html.toHtml @Text b.torrentGroupJson.groupName}
|
{Html.toHtml @Text b.torrentGroupJson.groupName}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{Html.toHtml @Text b.releaseType.unReleaseType}</td>
|
||||||
<td>{Html.toHtml @Natural b.torrentGroupJson.groupYear}</td>
|
<td>{Html.toHtml @Natural b.torrentGroupJson.groupYear}</td>
|
||||||
<td>{Html.toHtml @Int b.seedingWeight}</td>
|
<td>{Html.toHtml @Int b.seedingWeight}</td>
|
||||||
<td>{Html.toHtml @Text b.torrentFormat}</td>
|
<td>{Html.toHtml @Text b.torrentFormat}</td>
|
||||||
|
|
@ -646,7 +651,10 @@ mkBestTorrentsTable fresh = do
|
||||||
</tr>
|
</tr>
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
[hsx|
|
let section rows = do
|
||||||
|
let releaseType = rows & NonEmpty.head & (.releaseType.unReleaseType)
|
||||||
|
[hsx|
|
||||||
|
<h2>{releaseType}s</h2>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -654,6 +662,7 @@ mkBestTorrentsTable fresh = do
|
||||||
<th>Group ID</th>
|
<th>Group ID</th>
|
||||||
<th>Artist</th>
|
<th>Artist</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
<th>Year</th>
|
<th>Year</th>
|
||||||
<th>Weight</th>
|
<th>Weight</th>
|
||||||
<th>Format</th>
|
<th>Format</th>
|
||||||
|
|
@ -661,11 +670,23 @@ mkBestTorrentsTable fresh = do
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{bestRows}
|
{bestRows rows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
case fresh & nonEmpty of
|
||||||
|
Nothing -> [hsx|No torrents found|]
|
||||||
|
Just fresh' -> do
|
||||||
|
( if opts.groupByReleaseType
|
||||||
|
then
|
||||||
|
fresh'
|
||||||
|
& toList
|
||||||
|
& groupAllWithComparison ((.releaseType) >$< releaseTypeComparison)
|
||||||
|
else [fresh']
|
||||||
|
)
|
||||||
|
& foldMap section
|
||||||
|
|
||||||
mkLinkList :: [T2 "url" Text "content" Html] -> Html
|
mkLinkList :: [T2 "url" Text "content" Html] -> Html
|
||||||
mkLinkList xs =
|
mkLinkList xs =
|
||||||
xs
|
xs
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue