A music player that connects to your cloud/distributed storage.
at main 10 kB view raw
1module Tracks exposing (..) 2 3import Base64 4import List.Extra as List 5import Maybe.Extra as Maybe 6import Playlists exposing (Playlist, PlaylistTrackWithoutMetadata) 7import String.Ext as String 8import Time 9import Time.Ext as Time 10 11 12 13-- 🌳 14 15 16type alias Track = 17 { id : String 18 , insertedAt : Time.Posix 19 , path : String 20 , sourceId : String 21 , tags : Tags 22 } 23 24 25 26-- PIECES 27 28 29type alias Tags = 30 { disc : Int 31 , nr : Int 32 33 -- Main 34 , album : Maybe String 35 , artist : Maybe String 36 , title : String 37 38 -- Extra 39 , genre : Maybe String 40 , picture : Maybe String 41 , year : Maybe Int 42 } 43 44 45 46-- DERIVATIVES & SUPPLEMENTS 47 48 49type alias Cover = 50 { group : String 51 , identifiedTrackCover : IdentifiedTrack 52 , key : String 53 , sameAlbum : Bool 54 , sameArtist : Bool 55 , trackIds : List String 56 , tracks : List IdentifiedTrack 57 , variousArtists : Bool 58 } 59 60 61type alias Favourite = 62 { artist : Maybe String 63 , title : String 64 } 65 66 67type alias IdentifiedTrack = 68 ( Identifiers, Track ) 69 70 71type alias Identifiers = 72 { isFavourite : Bool 73 , isMissing : Bool 74 75 -- 76 , filename : String 77 , group : Maybe { name : String, firstInGroup : Bool } 78 , indexInList : Int 79 , indexInPlaylist : Maybe Int 80 , parentDirectory : String 81 } 82 83 84 85-- COLLECTIONS 86 87 88type alias Collection = 89 { untouched : List Track 90 91 -- `Track`s with `Identifiers` 92 , identified : List IdentifiedTrack 93 94 -- Sorted, grouped and filtered by playlist (if not auto-generated) 95 , arranged : List IdentifiedTrack 96 97 -- Filtered by search results, favourites, etc. 98 , harvested : List IdentifiedTrack 99 100 -- Contexts 101 ----------- 102 , scrollContext : String 103 } 104 105 106type alias CollectionDependencies = 107 { cached : List String 108 , cachedOnly : Bool 109 , enabledSourceIds : List String 110 , favourites : List Favourite 111 , favouritesOnly : Bool 112 , grouping : Maybe Grouping 113 , hideDuplicates : Bool 114 , selectedPlaylist : Maybe Playlist 115 , searchResults : Maybe (List String) 116 , sortBy : SortBy 117 , sortDirection : SortDirection 118 } 119 120 121type alias Parcel = 122 ( CollectionDependencies, Collection ) 123 124 125type alias CoverCollection = 126 { arranged : List Cover 127 , harvested : List Cover 128 } 129 130 131 132-- GROUPING & SORTING 133 134 135type Grouping 136 = AddedOn 137 | Directory 138 | FirstAlphaCharacter 139 | TrackYear 140 141 142type SortBy 143 = Artist 144 | Album 145 | PlaylistIndex 146 | Title 147 148 149type SortDirection 150 = Asc 151 | Desc 152 153 154 155-- VIEW 156 157 158type Scene 159 = Covers 160 | List 161 162 163 164-- 🔱 165 166 167emptyTrack : Track 168emptyTrack = 169 { id = "" 170 , insertedAt = Time.default 171 , path = "" 172 , sourceId = "" 173 , tags = emptyTags 174 } 175 176 177emptyTags : Tags 178emptyTags = 179 { disc = 1 180 , nr = 0 181 , album = Nothing 182 , artist = Nothing 183 , title = "Empty" 184 , genre = Nothing 185 , picture = Nothing 186 , year = Nothing 187 } 188 189 190emptyIdentifiedTrack : IdentifiedTrack 191emptyIdentifiedTrack = 192 ( emptyIdentifiers 193 , emptyTrack 194 ) 195 196 197emptyIdentifiers : Identifiers 198emptyIdentifiers = 199 { isFavourite = False 200 , isMissing = False 201 202 -- 203 , filename = "" 204 , group = Nothing 205 , indexInList = 0 206 , indexInPlaylist = Nothing 207 , parentDirectory = "" 208 } 209 210 211emptyCollection : Collection 212emptyCollection = 213 { untouched = [] 214 , identified = [] 215 , arranged = [] 216 , harvested = [] 217 218 -- Contexts 219 ----------- 220 , scrollContext = "" 221 } 222 223 224{-| If a track doesn't fit into a group, where does it go? 225-} 226fallbackCoverGroup : String 227fallbackCoverGroup = 228 "MISSING_TRACK_INFO" 229 230 231{-| This value is used as a fallback in the UI if the album is missing. 232-} 233fallbackAlbum : String 234fallbackAlbum = 235 "" 236 237 238{-| This value is used as a fallback in the UI if the artist is missing. 239-} 240fallbackArtist : String 241fallbackArtist = 242 "" 243 244 245 246-- MORE STUFF 247 248 249coverGroup : SortBy -> IdentifiedTrack -> String 250coverGroup sort ( identifiers, { tags } as track ) = 251 if identifiers.isMissing then 252 "MISSING_TRACKS" 253 254 else 255 case sort of 256 Artist -> 257 Maybe.unwrap fallbackCoverGroup (String.trim >> String.toLower) tags.artist 258 259 Album -> 260 -- There is the possibility of albums with the same name, 261 -- such as "Greatests Hits". 262 -- To make sure we treat those as different albums, 263 -- we prefix the album by its parent directory. 264 case tags.album of 265 Just album -> 266 (identifiers.parentDirectory ++ album) 267 |> String.trim 268 |> String.toLower 269 270 Nothing -> 271 fallbackCoverGroup 272 273 PlaylistIndex -> 274 "" 275 276 Title -> 277 tags.title 278 279 280coverKey : Bool -> Track -> String 281coverKey isVariousArtists { tags } = 282 if isVariousArtists then 283 Maybe.withDefault "?" tags.album 284 285 else 286 Maybe.withDefault "?" tags.artist ++ " --- " ++ Maybe.withDefault "?" tags.album 287 288 289isNowPlaying : IdentifiedTrack -> IdentifiedTrack -> Bool 290isNowPlaying ( a, b ) ( x, y ) = 291 a.indexInPlaylist == x.indexInPlaylist && b.id == y.id 292 293 294makeTrack : String -> ( String, Tags ) -> Track 295makeTrack sourceId ( path, tags ) = 296 { id = 297 (sourceId ++ "//" ++ path) 298 |> Base64.encode 299 |> String.chopEnd "=" 300 , insertedAt = Time.default 301 , path = path 302 , sourceId = sourceId 303 , tags = tags 304 } 305 306 307matchesAutoGeneratedPlaylist : Playlist -> Track -> Bool 308matchesAutoGeneratedPlaylist playlist track = 309 case playlist.autoGenerated of 310 Just { level } -> 311 track.path 312 |> String.split "/" 313 |> List.drop (max 0 (level - 1)) 314 |> List.head 315 |> (==) (Just playlist.name) 316 317 Nothing -> 318 False 319 320 321missingId : String 322missingId = 323 "<missing>" 324 325 326pathParts : Track -> { filename : String, parentDirectory : String } 327pathParts { path } = 328 let 329 s = 330 String.split "/" path 331 332 l = 333 List.length s 334 in 335 case List.drop (max 0 <| l - 2) s of 336 [ p, f ] -> 337 { filename = f, parentDirectory = p } 338 339 [ f ] -> 340 { filename = f, parentDirectory = "" } 341 342 _ -> 343 { filename = "", parentDirectory = "" } 344 345 346{-| Given a collection of tracks, pick out the tracks by id in order. 347Note that track ids in the ids list may occur multiple times. 348-} 349pick : List String -> List Track -> List Track 350pick ids collection = 351 collection 352 |> List.foldr 353 (\track -> 354 List.map 355 (\picking -> 356 case picking of 357 PickId id -> 358 if id == track.id then 359 PickTrack track 360 361 else 362 PickId id 363 364 p -> 365 p 366 ) 367 ) 368 (List.map PickId ids) 369 |> List.foldr 370 (\picking acc -> 371 case picking of 372 PickId _ -> 373 acc 374 375 PickTrack track -> 376 track :: acc 377 ) 378 [] 379 380 381removeByPaths : { sourceId : String, paths : List String } -> List Track -> { kept : List Track, removed : List Track } 382removeByPaths { sourceId, paths } tracks = 383 tracks 384 |> List.foldr 385 (\t ( kept, removed, remainingPathsToRemove ) -> 386 if t.sourceId == sourceId && List.member t.path remainingPathsToRemove then 387 ( kept, t :: removed, List.remove t.path remainingPathsToRemove ) 388 389 else 390 ( t :: kept, removed, remainingPathsToRemove ) 391 ) 392 ( [], [], paths ) 393 |> (\( k, r, _ ) -> 394 { kept = k, removed = r } 395 ) 396 397 398removeBySourceId : String -> List Track -> { kept : List Track, removed : List Track } 399removeBySourceId removedSourceId tracks = 400 tracks 401 |> List.foldr 402 (\t ( kept, removed ) -> 403 if t.sourceId == removedSourceId then 404 ( kept, t :: removed ) 405 406 else 407 ( t :: kept, removed ) 408 ) 409 ( [], [] ) 410 |> (\( k, r ) -> 411 { kept = k, removed = r } 412 ) 413 414 415removeFromPlaylist : List IdentifiedTrack -> Playlist -> Playlist 416removeFromPlaylist tracks playlist = 417 playlist.tracks 418 |> List.indexedFoldr 419 (\idx t ( acc, remaining ) -> 420 case List.partition ((==) (Just idx)) remaining of 421 ( _ :: _, rem ) -> 422 ( acc, rem ) 423 424 ( _, rem ) -> 425 ( t :: acc, rem ) 426 ) 427 ( [] 428 , List.map (Tuple.first >> .indexInPlaylist) tracks 429 ) 430 |> (\( t, _ ) -> { playlist | tracks = t }) 431 432 433shouldNoteProgress : { duration : Float } -> Bool 434shouldNoteProgress { duration } = 435 duration >= 30 * 60 436 437 438shouldRenderGroup : Identifiers -> Bool 439shouldRenderGroup identifiers = 440 identifiers.group 441 |> Maybe.map (.firstInGroup >> (==) True) 442 |> Maybe.withDefault False 443 444 445playlistTrackFromTrack : Track -> PlaylistTrackWithoutMetadata 446playlistTrackFromTrack track = 447 { album = track.tags.album 448 , artist = track.tags.artist 449 , title = track.tags.title 450 } 451 452 453sortByIndexInPlaylist : List IdentifiedTrack -> List IdentifiedTrack 454sortByIndexInPlaylist = 455 List.sortBy (\( i, t ) -> Maybe.withDefault (t.tags.disc * 1000 + t.tags.nr) i.indexInPlaylist) 456 457 458toPlaylistTracks : List IdentifiedTrack -> List PlaylistTrackWithoutMetadata 459toPlaylistTracks = 460 List.map (Tuple.second >> playlistTrackFromTrack) 461 462 463 464-- ㊙️ 465 466 467type Pick 468 = PickId String 469 | PickTrack Track