diff --git a/JlSonic/api.jl b/JlSonic/api.jl index d28060f..b34a6d7 100644 --- a/JlSonic/api.jl +++ b/JlSonic/api.jl @@ -61,54 +61,48 @@ function getMusicFolders() end end +# Implement: +# getIndexes +# getMusicDirectory + "Returns all genres." function getGenres() - @subsonic begin - songs = allsongs(); - res = Dict{String,Int}() - for genre in filter!(!isempty, getfield.(songs, :genre)) - t = get(res, genre, 0) - res[genre] = t+1 - end - genres = new_child(xroot, "genres") - for k in keys(res) - genre = new_child(genres, "genre") - set_attributes(genre, [ - ("songCount", string(res[k])), - # FIXME - ("albumCount", "0"), - ]) - add_text(genre, k) - end + (xdoc, xroot) = subsonic() + songs = Beets.songs(); + res = Dict{String,Int}() + genrelist = strip.(sort(filter!(!isempty, Beets.genre.(Beets.albums)))) + for genre in genrelist + t = get(res, genre, 0) + res[genre] = t+1 end + genres = new_child(xroot, "genres") + for k in keys(res) + genre = new_child(genres, "genre") + set_attributes(genre, [ + ("songCount", string(res[k])), + # FIXME + ("albumCount", string(count(genrelist .== k))), + ]) + add_text(genre, k) + end + return subsonic_return(xdoc) end "Similar to getIndexes, but organizes music according to ID3 tags." function getArtists() - # TODO - (xdoc, xroot) = subsonic(version = "1.12.0") + (xdoc, xroot) = subsonic() indexes = new_child(xroot, "artists") set_attribute(indexes, "ignoredArticles", "") - beetsdb = Beets.getartists() - artists = unique(beetsdb) - # albums = group_albums_as_artists() - # .|> does not work in a macro. What to do? - for index in unique(first.(filter(!isempty, - getfield.(artists, Ref(:name)))) .|> uppercase) + artists = Beets.artists() + firstletters = unique(first.(filter(!isempty, Beets.name.(artists))) .|> uppercase) + for index in string.(firstletters) indexXML = new_child(indexes, "index") - set_attribute(indexXML, "name", string(index)) - for artist in filter(x -> startswith(x.name, string(index)), artists) - artistXML = new_child(indexXML, "artist") - set_attributes(artistXML, - [("name", artist.name), - ("id", artist.uuid), - ("coverArt", ""), - ("albumCount", "")]) + set_attribute(indexXML, "name", index) + for artist in unique(filter(x -> startswith(x.name, index), artists)) + artistXML = push!(indexXML, artist) end end - doc_str = string(xdoc) - free(xdoc) - return doc_str + return subsonic_return(xdoc) end """Returns details for an artist, including a list of albums. @@ -116,22 +110,14 @@ end This method organizes music according to ID3 tags.""" function getArtist(id::String) artists = Beets.getartists() - matching = artists[getfield.(artists, :uuid) .== id] - name = length(matching) > 0 ? first(matching).name : "" - isempty(name) && return not_found() + matching = findfirst(a -> a.uuid == id, artists) + matching === nothing && return not_found("id") + artist = artists[matching] # Create the response (xdoc, xroot) = subsonic() - artistXML = new_child(xroot, "artist") - artist = first(matching) - artist_albums = [i for i in Beets.getalbums() if i.artist == artist] - set_attributes(artistXML, [ - ("id", artist.uuid), - ("albumCount", string(length(artist_albums))), - ("name", artist.name), - ("coverArt", "false") - ]) - for album in artist_albums - push!(xroot, album) + artistXML = push!(xroot, artist) + for album in Beets.albums(artist) + push!(artistXML, album) end return subsonic_return(xdoc) end @@ -150,9 +136,17 @@ function getAlbum(req::Dict) return getAlbum(albumid) end +function getAlbum(albumid) + album = Beets.album(string(albumid)) + album === nothing && return not_found("album") + (xdoc, xroot) = subsonic() + # push!(albumXML, [(s, album) for s in album.songs]) + append!(xroot, album) + return subsonic_return(xdoc) +end + function getAlbumList(req::Dict) query = HTTP.URIs.queryparams(req[:query]) - @show query albumtype = get(query, "type", "") isempty(albumtype) && return missing_parameter("type") @subsonic begin @@ -161,18 +155,6 @@ function getAlbumList(req::Dict) end end -function getAlbum(albumid) - matching = [album for album in Beets.getalbums() if album.uuid == albumid] - if length(matching) < 1 - return not_found("album") - end - album = first(matching) - (xdoc, xroot) = subsonic() - albumXML = push!(xroot, album) - push!(albumXML, [(s, album) for s in album.songs]) - return subsonic_return(xdoc) -end - function getSong(req) query = HTTP.URIs.queryparams(req[:query]) id = get(query, "id", "") @@ -192,7 +174,7 @@ function getRandomSongs(; size = 10, fromYear::Union{Missing,Int} = missing, toYear::Union{Missing,Int} = missing, musicFolderId::Union{Missing,String} = missing) - songs = allsongs(); + songs = Beets.songs(); # Randomize songs = songs[Random.randperm(length(songs))]; # Filter @@ -299,7 +281,7 @@ function createPlaylist(req) songId = get(query, "songId", "") # Check required params isempty(songId) && return missing_parameter("songId") - songs = allsongs(); + songs = Beets.songs(); songn = findfirst(s -> s.uuid == songId, songs) songn === nothing && return not_found("songId") song = songs[songn] @@ -355,7 +337,6 @@ end function updatePlaylist(req) global user_playlists query = HTTP.URIs.queryparams(req[:query]) - @show query playlistId = get(query, "playlistId", "") isempty(playlistId) && return missing_parameter("playlistId") # FIXME: check ownership @@ -374,7 +355,7 @@ function updatePlaylist(req) # TODO: Support multiple (repeated) parameter if !isempty(songIdAdd) - songs = allsongs(); + songs = Beets.songs(); songn = findfirst(s -> s.uuid == songIdAdd, songs) songn === nothing && return not_found("songIdToAdd") song = songs[songn] @@ -405,17 +386,15 @@ function getUser(req) end # Media retriveal - "Returns a cover art image." function getCoverArt(req::Dict) query = HTTP.URIs.queryparams(req[:query]) id = get(query, "id", "") isempty(id) && return missing_parameter("id") - albums = Beets.getalbums() - n = findfirst(a -> album.uuid == id, albums) + n = findfirst(a -> a.uuid == id, Beets.albums) n === nothing && return not_found("id") - # @show matching.cover - return Dict(:body => read(albums)) + headers = Dict{String,String}() + return sendfile(Beets.albums[n].cover) end "Streams a given media file." @@ -423,8 +402,19 @@ function stream(req::Dict) query = HTTP.URIs.queryparams(req[:query]) id = get(query, "id", "") isempty(id) && return missing_parameter("id") - songs = allsongs() + songs = Beets.songs() m = findfirst(x -> (x.uuid == id), songs) m === nothing && return not_found("id") - return Dict(:body => read(songs[m].path)) + + return sendfile(songs[m].path) +end + +function sendfile(path; suffix = nothing) + isfile(path) || return Dict{String,String}(:body => "Not Found") + suffix = suffix == nothing ? lowercase(split(path, '.')[end]) : suffix + headers = Dict{String,String}() + headers["Content-Type"] = Mux.mimetypes[suffix] + headers["Content-Length"] = string(filesize(path)) + return Dict(:body => read(path), + :headers => headers) end diff --git a/JlSonic/beet2xml.jl b/JlSonic/beet2xml.jl index 8dc710b..eb158e4 100644 --- a/JlSonic/beet2xml.jl +++ b/JlSonic/beet2xml.jl @@ -9,7 +9,7 @@ function push!(root::XMLElement, p::Playlist) ("owner", p.owner), ("public", string(p.public)), ("songCount", string(length(p.songs))), - ("duration", reduce(+, p.songs, init = 0.0) |> floor |> string), + ("duration", reduce(+, p.songs, init = 0.0) |> floor |> Int |> string), ("created", "FIXME"), ("coverArt", p.cover), ]) @@ -35,38 +35,72 @@ push!(p::Playlist, s::Song) = push!(p.songs, s) function push!(root::XMLElement, album::Beets.Album) albumXML = new_child(root, "album") set_attributes(albumXML, [ - ("name", album.title), ("id", album.uuid), - ("name", album.artist.name), - ("artistId", album.artist.uuid), - ("artist", album.artist.name), - # FIXME - ("created", "0"), + ("name", album.title), ("coverArt", album.uuid), - ("songs", "FIXME"), ("songCount", string(length(album.songs))), - ("duration", string(sum([t.length for t in album.songs]))) + ("created", "0"), # FIXME + ("duration", string(sum([t.length for t in album.songs]) |> floor |> Int)), + ("artist", album.artist.name), + ("artistId", album.artist.uuid) ]) albumXML end +function append!(root::XMLElement, a::Beets.Album) + albumXML = push!(root, a) + for song in a.songs + songXML = push!(albumXML, song) + set_attributes(songXML, [ + ("album", a.title), + ("parent", a.artist.uuid), # Not clear + ("artist", a.artist.name), + ("coverArt", a.uuid), + ("albumId", a.uuid), + ("artistId", a.artist.uuid), + ]) + end + albumXML +end + function push!(root::XMLElement, artist::Beets.Artist) artistXML = new_child(root, "artist") set_attributes(artistXML, [ ("id", artist.uuid), ("name", artist.name), ("coverArt", artist.uuid), - ("albumCount", "0") + ("albumCount", Beets.albums(artist) |> length |> string) ]) artistXML end +function push!(root::XMLElement, song::Beets.Song) + songXML = new_child(root, "song") + suffix = lowercase(song.format) + set_attributes(songXML, [ + ("id", song.uuid), + ("title", song.title), + ("isDir", "false"), + ("created", "FIXME"), + ("duration", string(floor(song.length) |> Int)), + ("bitrate", string(song.bitrate)), + ("size", string(filesize(song.path))), + ("suffix", suffix), + ("contentType", Mux.mimetypes[suffix]), + ("isVideo", "false"), + ("path", relpath(song.path, Beets.musicdir())), + ("type", "music") + ]) + songXML +end + function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album}) songXML = new_child(root, "song") song, album = song_album + suffix = lowercase(song.format) set_attributes(songXML, [ ("id", song.uuid), - # ("parent", album.artist.uuid), # Not clear + ("parent", album.artist.uuid), # Not clear ("title", song.title), ("album", album.title), ("artist", album.artist.name), @@ -76,9 +110,8 @@ function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album}) ("duration", string(floor(song.length) |> Int)), ("bitrate", string(song.bitrate)), ("size", string(filesize(song.path))), - ("suffix", lowercase(song.format)), - ## FIXME - ("contentType", "audio/flac"), # mpeg + ("suffix", suffix), + ("contentType", Mux.mimetypes[suffix]), # mpeg ("isVideo", "false"), ("path", relpath(song.path, Beets.musicdir())), ("albumId", album.uuid), @@ -89,6 +122,7 @@ function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album}) end function props(song::Song) + suffix = lowercase(song.format) [ ("id", song.uuid), # ("parent", album.artist.uuid), # Not clear @@ -101,9 +135,8 @@ function props(song::Song) ("duration", string(floor(song.length) |> Int)), ("bitrate", string(song.bitrate)), ("size", string(filesize(song.path))), - ("suffix", lowercase(song.format)), - ## FIXME - ("contentType", "audio/flac"), # mpeg + ("suffix", suffix), + ("contentType", Mux.mimetypes[suffix]), # mpeg ("isVideo", "false"), ("path", relpath(song.path, Beets.musicdir())), # ("albumId", song.album.uuid), diff --git a/server.jl b/server.jl index 8441db2..e050455 100644 --- a/server.jl +++ b/server.jl @@ -2,10 +2,13 @@ using Mux using HTTP using Revise +push!(LOAD_PATH, "/home/nixo/memories/projects/2018-2019/musicjl") import Beets +Beets.update_albums(); push!(LOAD_PATH, realpath("JlSonic")) using JlSonic + include("router.jl") include("login.jl") @app sonic = ( @@ -21,3 +24,4 @@ if !isdefined(Main, :started) serve(sonic) started = true end +