commit - c4b7ce378af0cfffac7da1830a9a4f093e9e449c
commit + cffaa503c72d25adc9062981bf1be8aa74451407
blob - cde7042199852340b13d1dbf1f9b8463fb00801a
blob + 001c24e5c57eb72c00ca4637507b4ad114198748
--- README.md
+++ README.md
-# corpus
+<div align="center">
+<img src="assets/cover.webp" alt="corpus" width="480">
+
+</div>
+
[](https://builds.sr.ht/~mtmn/corpus?)
A self-hosted [ListenBrainz](https://listenbrainz.org) and [Last.fm](https://last.fm) frontend that stores metadata and cover images.
blob - /dev/null
blob + 5e524a83f88b6c588a417e24969caeaa79795edd (mode 644)
Binary files /dev/null and assets/cover.webp differ
blob - 0b82d68ca944666469e4faf27dd30f8e7744eddf
blob + 117e5c6c11447dfb307158ca9cf0565adbddf45d
--- src/Api.elm
+++ src/Api.elm
"/stats?user=" ++ userSlug ++ periodPart ++ sectionPart
-fetchSimilarTracks : String -> String -> Cmd Msg
-fetchSimilarTracks artist track =
+fetchSimilarTracks : Int -> String -> String -> Cmd Msg
+fetchSimilarTracks idx artist track =
Http.get
{ url = "/similar?artist=" ++ Url.percentEncode artist ++ "&track=" ++ Url.percentEncode track
- , expect = Http.expectJson (FetchSimilarTracks artist track) similarTracksDecoder
+ , expect = Http.expectJson (FetchSimilarTracks idx artist track) similarTracksDecoder
}
blob - f93a782aaa84327b39b9e0e8969439f641a4b235
blob + dcb433e04dac789877f618236040b9f0cc9874ec
--- src/Main.purs
+++ src/Main.purs
serveClientJs res
"/favicon.png" ->
serveAsset "image/png" "assets/favicon.png" res
+ "/cover.webp" ->
+ serveAsset "image/webp" "assets/cover.webp" res
"/" ->
serveIndex allUsers "" res
"/metrics" ->
blob - 624837037fccbed2979f91d77a611d1d825c2882
blob + 28cce86e825e08eb3309f5200d7d2c3a2838f295
--- src/State.elm
+++ src/State.elm
, allUsers = flags.allUsers
, searchInput = ""
, activeSearch = Nothing
+ , tabsVisible = False
}
, Cmd.batch
[ fetchListens flags.userSlug 25 offset Nothing Nothing
Cmd.none
)
+ ToggleTabs ->
+ ( { model | tabsVisible = not model.tabsVisible }, Cmd.none )
+
SetStatsPeriod period ->
( { model
| statsPeriod = period
, Cmd.none
)
- FetchSimilar artist track ->
+ FetchSimilar idx artist track ->
let
key =
- artist ++ "\t" ++ track
+ String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ track
in
if Dict.member key model.similarStates then
( { model | similarStates = Dict.remove key model.similarStates }, Cmd.none )
else
( { model | similarStates = Dict.insert key SimilarLoading model.similarStates }
- , fetchSimilarTracks artist track
+ , fetchSimilarTracks idx artist track
)
- FetchSimilarTracks artist track result ->
+ FetchSimilarTracks idx artist track result ->
let
key =
- artist ++ "\t" ++ track
+ String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ track
newStates =
case result of
blob - b830780c2891d7507c99d98fca7100a840d7f200
blob + 54bedbe5c6371112153fdc17601db6681b0bb8db
--- src/Templates.purs
+++ src/Templates.purs
<style>
body {
font-family: 'Intel One Mono', 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: #181818;
- color: #e7e7e7;
+ background: oklch(0.194 0.0345 225.31);
+ color: oklch(0.91 0.012 225.31);
margin: 0;
line-height: 1.6;
}
::selection {
- background: #505050;
- color: #e7e7e7;
+ background: oklch(0.42 0.04 225.31);
+ color: oklch(0.91 0.012 225.31);
}
.container {
}
h1 {
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
margin: 0 0 20px 0;
font-size: 24px;
- border-bottom: 2px solid #505050;
+ border-bottom: 2px solid oklch(0.42 0.04 225.31);
padding-bottom: 8px;
}
+ h1.site-header {
+ border-bottom: none;
+ padding-bottom: 0;
+ line-height: 0;
+ cursor: pointer;
+ }
+
+ h1.site-header img {
+ display: block;
+ width: 100%;
+ height: 80px;
+ object-fit: cover;
+ border-radius: 4px;
+ border: 1px solid oklch(0.42 0.04 225.31);
+ }
+
ul {
list-style: none;
padding: 0;
}
li {
- background: #2f2f2f;
- border: 1px solid #505050;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
border-radius: 4px;
padding: 5px 15px 5px 15px;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
- box-shadow: 4px 4px 0px #505050;
+ box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31);
}
li.success {
- background: #2f2f2f;
- border-color: #505050;
- box-shadow: 4px 4px 0px #505050;
+ background: oklch(0.27 0.038 225.31);
+ border-color: oklch(0.42 0.04 225.31);
+ box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31);
}
.track-info {
.track-name {
font-weight: bold;
font-size: 16px;
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
}
.track-artist {
font-size: 14px;
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
margin-top: 1px;
}
.track-time {
font-size: 12px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
margin-top: 2px;
}
background: none;
border: none;
padding: 0;
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
font-weight: bold;
font-size: 16px;
font-family: inherit;
background: none;
border: none;
padding: 0;
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
font-size: 14px;
font-family: inherit;
cursor: pointer;
background: none;
border: none;
padding: 0;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-decoration: underline;
font-family: inherit;
font-size: inherit;
}
.album-link:hover {
- color: #e7e7e7;
+ color: oklch(0.78 0.13 357.86);
}
.label-link {
background: none;
border: none;
padding: 0;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-decoration: underline;
font-family: inherit;
font-size: inherit;
}
.label-link:hover {
- color: #e7e7e7;
+ color: oklch(0.78 0.13 357.86);
}
.track-cover {
.loading {
padding: 20px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-align: center;
}
.error {
padding: 20px;
- color: #eca28f;
+ color: oklch(0.78 0.085 225.31);
text-align: center;
}
.small {
font-size: 12px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
margin-top: 20px;
}
.small a {
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
text-decoration: none;
}
.small a:hover {
- color: #e7e7e7;
+ color: oklch(0.78 0.13 357.86);
text-decoration: underline;
}
}
.page-btn {
- background: #2f2f2f;
- border: 1px solid #505050;
- color: #e7e7e7;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.91 0.012 225.31);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
- box-shadow: 2px 2px 0px #505050;
+ box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
}
.page-btn:hover {
- background: #505050;
- border-color: #505050;
+ background: oklch(0.32 0.045 225.31);
+ border-color: oklch(0.52 0.05 225.31);
}
.page-btn:disabled {
display: flex;
align-items: center;
font-size: 14px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
}
.tabs {
.tab-btn {
background: none;
- border: 1px solid #505050;
- color: #9fbfe7;
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.74 0.075 225.31);
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
}
.tab-btn.active {
- background: #2f2f2f;
- color: #e7e7e7;
- box-shadow: 2px 2px 0px #505050;
+ background: oklch(0.27 0.038 225.31);
+ color: oklch(0.91 0.012 225.31);
+ box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
}
.tab-btn:hover {
- color: #e7e7e7;
- background: #505050;
+ color: oklch(0.91 0.012 225.31);
+ background: oklch(0.42 0.04 225.31);
}
.stats-section {
.stats-section h2 {
font-size: 11px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-transform: uppercase;
letter-spacing: 3px;
margin: 0 0 10px 0;
- border-bottom: 1px solid #505050;
+ border-bottom: 1px solid oklch(0.42 0.04 225.31);
padding-bottom: 5px;
}
left: 0;
top: 0;
height: 100%;
- background: #2f2f2f;
- border-right: 1px solid #505050;
+ background: oklch(0.27 0.038 225.31);
+ border-right: 1px solid oklch(0.42 0.04 225.31);
z-index: 0;
}
.stat-name {
position: relative;
z-index: 1;
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
flex: 1;
padding-right: 10px;
white-space: nowrap;
.stat-count {
position: relative;
z-index: 1;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
font-size: 12px;
flex-shrink: 0;
}
.stats-empty {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
font-size: 13px;
padding: 10px 0;
}
}
.stat-row.clickable:hover .stat-name {
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
}
.filter-banner {
display: flex;
align-items: center;
gap: 10px;
- background: #2f2f2f;
- border: 1px solid #505050;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
border-radius: 4px;
padding: 8px 12px;
margin-bottom: 12px;
font-size: 13px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
}
.filter-label {
}
.filter-label strong {
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
}
.filter-clear {
background: none;
- border: 1px solid #505050;
- color: #9fbfe7;
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.74 0.075 225.31);
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
}
.filter-clear:hover {
- color: #e7e7e7;
- border-color: #e7e7e7;
- background: #505050;
+ color: oklch(0.91 0.012 225.31);
+ border-color: oklch(0.52 0.05 225.31);
+ background: oklch(0.32 0.045 225.31);
}
.show-all-btn {
background: none;
border: none;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
cursor: pointer;
font-family: inherit;
font-size: 12px;
}
.show-all-btn:hover {
- color: #e7e7e7;
- background: #505050;
+ color: oklch(0.91 0.012 225.31);
+ background: oklch(0.32 0.045 225.31);
}
.period-selector {
.period-btn {
background: none;
- border: 1px solid #505050;
- color: #9fbfe7;
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.74 0.075 225.31);
padding: 4px 12px;
border-radius: 3px;
cursor: pointer;
}
.period-btn:hover {
- color: #e7e7e7;
- border-color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
+ border-color: oklch(0.7 0.1274 357.86);
}
.period-btn.active {
- background: #505050;
- color: #e7e7e7;
+ background: oklch(0.5 0.1274 357.86);
+ border-color: oklch(0.7 0.1274 357.86);
+ color: oklch(0.97 0.01 357.86);
}
.custom-range {
}
.custom-range-input {
- background: #2f2f2f;
- border: 1px solid #505050;
- color: #e7e7e7;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.91 0.012 225.31);
padding: 4px 8px;
border-radius: 3px;
font-family: inherit;
}
.custom-range-input::placeholder {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
opacity: 0.7;
}
.custom-range-input:focus {
outline: none;
- border-color: #e7e7e7;
+ border-color: oklch(0.91 0.012 225.31);
}
.custom-range-input.error {
- border-color: #ff6b6b;
+ border-color: oklch(0.68 0.11 225.31);
}
.custom-range-error {
- color: #ff6b6b;
+ color: oklch(0.68 0.11 225.31);
font-size: 12px;
margin-top: 4px;
}
.similar-btn {
background: none;
- border: 1px solid #505050;
- color: #9fbfe7;
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.74 0.075 225.31);
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
}
.similar-btn:hover {
- background: #505050;
- color: #e7e7e7;
- border-color: #e7e7e7;
+ background: oklch(0.32 0.045 225.31);
+ color: oklch(0.91 0.012 225.31);
+ border-color: oklch(0.7 0.1274 357.86);
}
.similar-btn.active {
- background: #2f2f2f;
- color: #e7e7e7;
- border-color: #505050;
- box-shadow: 2px 2px 0px #505050;
+ background: oklch(0.5 0.1274 357.86);
+ color: oklch(0.97 0.01 357.86);
+ border-color: oklch(0.7 0.1274 357.86);
+ box-shadow: 2px 2px 0px oklch(0.7 0.1274 357.86);
}
.similar-panel {
margin-top: 10px;
- background: #2f2f2f;
- border: 1px solid #505050;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
border-radius: 4px;
padding: 10px;
- box-shadow: 2px 2px 0px #505050;
+ box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
}
.similar-panel-header {
font-size: 11px;
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.similar-loading {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
font-size: 12px;
text-align: center;
padding: 8px;
}
.similar-error {
- color: #eca28f;
+ color: oklch(0.78 0.085 225.31);
font-size: 12px;
text-align: center;
padding: 8px;
}
.similar-empty {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
font-size: 12px;
text-align: center;
padding: 8px;
align-items: center;
padding: 6px 8px;
margin-bottom: 4px;
- background: #2f2f2f;
+ background: oklch(0.27 0.038 225.31);
border-radius: 3px;
- border-left: 2px solid #505050;
+ border-left: 2px solid oklch(0.42 0.04 225.31);
}
.similar-track:last-child {
.similar-track-name {
font-size: 12px;
- color: #e7e7e7;
+ color: oklch(0.91 0.012 225.31);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
.similar-track-artist {
font-size: 11px;
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
.similar-score {
font-size: 10px;
- color: #9fbfe7;
- background: #2f2f2f;
- border: 1px solid #505050;
+ color: oklch(0.74 0.075 225.31);
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
padding: 2px 6px;
border-radius: 2px;
margin-right: 8px;
}
.similar-link {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-decoration: none;
font-size: 12px;
- background: #2f2f2f;
- border: 1px solid #505050;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
padding: 2px 6px;
border-radius: 2px;
transition: all 0.2s ease;
}
.similar-link:hover {
- color: #e7e7e7;
- background: #505050;
+ color: oklch(0.78 0.13 357.86);
+ background: oklch(0.42 0.04 225.31);
}
.tracks-with-similar {
.search-input {
flex: 1;
- background: #2f2f2f;
- border: 1px solid #505050;
- color: #e7e7e7;
+ background: oklch(0.27 0.038 225.31);
+ border: 1px solid oklch(0.42 0.04 225.31);
+ color: oklch(0.91 0.012 225.31);
padding: 6px 10px;
border-radius: 4px;
font-family: inherit;
}
.search-input::placeholder {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
opacity: 0.7;
}
.search-input:focus {
outline: none;
- border-color: #e7e7e7;
+ border-color: oklch(0.91 0.012 225.31);
}
.search-btn {
- background: #505050;
- border: 1px solid #505050;
- color: #e7e7e7;
+ background: oklch(0.32 0.045 225.31);
+ border: 1px solid oklch(0.52 0.05 225.31);
+ color: oklch(0.91 0.012 225.31);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.search-btn:hover {
- background: #707070;
- border-color: #707070;
+ background: oklch(0.42 0.05 225.31);
+ border-color: oklch(0.52 0.05 225.31);
}
+ .search-bar:focus-within .search-btn {
+ background: oklch(0.5 0.1274 357.86);
+ border-color: oklch(0.7 0.1274 357.86);
+ color: oklch(0.97 0.01 357.86);
+ }
+
.about-lead {
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
font-size: 13px;
line-height: 1.8;
margin: 0 0 30px 0;
}
.about-link {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-decoration: underline;
}
.about-link:hover {
- color: #e7e7e7;
+ color: oklch(0.78 0.13 357.86);
}
.about-list {
display: block;
box-shadow: none;
font-size: 13px;
- color: #a0c0d0;
+ color: oklch(0.70 0.035 225.31);
}
.about-list li::before {
content: "→ ";
- color: #505050;
+ color: oklch(0.42 0.04 225.31);
}
.about-meta {
}
.about-users a {
- color: #9fbfe7;
+ color: oklch(0.74 0.075 225.31);
text-decoration: none;
font-size: 13px;
}
.about-users a:hover {
- color: #e7e7e7;
+ color: oklch(0.78 0.13 357.86);
text-decoration: underline;
}
</style>
blob - 7adb3ac7c482467cf4d8f4ce220366aeb3139639
blob + 997f7c0a7393abaea3ae558fcede5d6a046d840f
--- src/Types.elm
+++ src/Types.elm
, allUsers : List UserInfo
, searchInput : String
, activeSearch : Maybe String
+ , tabsVisible : Bool
}
| NextPage
| PrevPage
| SwitchTab Tab
+ | ToggleTabs
| SetStatsPeriod Period
| OpenCustomInput
| UpdateCustomInput String
| ExpandSection String
| ShowAllSection String
| CollapseSection String
- | FetchSimilar String String
- | FetchSimilarTracks String String (Result Http.Error (List SimilarTrack))
+ | FetchSimilar Int String String
+ | FetchSimilarTracks Int String String (Result Http.Error (List SimilarTrack))
| UpdateSearchInput String
| SubmitSearch
| ClearSearch
blob - 8835b814d54daec751ea92dc61b6398a045325a2
blob + 2324b16003998db561d027d6488ba2b22d5513e0
--- src/View.elm
+++ src/View.elm
{ title = "scrobbler"
, body =
[ div [ class "container" ]
- [ h1 [] [ text "scrobbler" ]
- , div [ class "tabs" ]
+ [ h1 [ class "site-header", onClick ToggleTabs ]
+ [ img [ src "/cover.webp", Attr.alt "scrobbler" ] [] ]
+ , if model.tabsVisible then
+ div [ class "tabs" ]
[ a
[ class
("tab-btn"
]
[ text "about" ]
]
+
+ else
+ text ""
, case model.activeTab of
ListensTab ->
div []
Maybe.withDefault "" listen.trackName
key =
- artist ++ "\t" ++ trackName
+ String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ trackName
in
div [ class "track-with-similar-container" ]
[ ul [ class "track-item" ]
Maybe.withDefault "" listen.releaseName
key =
- artist ++ "\t" ++ trackName
+ String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ trackName
mbid =
case listen.caaReleaseMbid of
""
)
)
- , onClick (FetchSimilar artist trackName)
+ , onClick (FetchSimilar idx artist trackName)
]
[ text "similar" ]