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