From c7a7dbd7faa21e5ec80280181bea3223f196c5a2 Mon Sep 17 00:00:00 2001 From: nixo Date: Sun, 19 May 2019 22:24:18 +0200 Subject: [PATCH] allow saving users and playlists --- JlSonic/JlSonic.jl | 1 + JlSonic/api.jl | 46 +++++++++++++++++++++++++++++++++++++-------- JlSonic/beet2xml.jl | 15 ++++++++------- JlSonic/types.jl | 31 ++++++++++++++++++------------ airsonic.rest | 14 +++++++++++--- login.jl | 39 ++++++++++++++++++++++++++++++-------- server.jl | 5 +++-- 7 files changed, 111 insertions(+), 40 deletions(-) diff --git a/JlSonic/JlSonic.jl b/JlSonic/JlSonic.jl index b98cddb..ecfafe6 100644 --- a/JlSonic/JlSonic.jl +++ b/JlSonic/JlSonic.jl @@ -5,6 +5,7 @@ using Beets using LightXML import UUIDs import HTTP +using JSON2 const domain = "nixo.xyz" diff --git a/JlSonic/api.jl b/JlSonic/api.jl index b34a6d7..c764dbc 100644 --- a/JlSonic/api.jl +++ b/JlSonic/api.jl @@ -116,7 +116,7 @@ function getArtist(id::String) # Create the response (xdoc, xroot) = subsonic() artistXML = push!(xroot, artist) - for album in Beets.albums(artist) + for album in Beets.album(artist) push!(artistXML, album) end return subsonic_return(xdoc) @@ -292,7 +292,7 @@ function createPlaylist(req) return not_found("playlistId") end elseif !isempty(name) - playlist = Playlist(req[:login][:name], name = name) + playlist = Playlist(req[:login][:user].name, name = name) push!(playlist, song) else return missing_parameter("either name or playlistId") @@ -301,17 +301,19 @@ function createPlaylist(req) # Return the playlist (xdoc, xroot) = subsonic() push!(xroot, playlist) + saveplaylists() return subsonic_return(xdoc) end "Returns all playlists a user is allowed to play." function getPlaylists(req) global user_playlists - user = req[:login][:name] # FIXME: add support for admin (ask other user's playlists, v 1.8.0) (xdoc, xroot) = subsonic(version = "1.0.0") playlistsXML = new_child(xroot, "playlists") - for playlist in filter(p -> p.owner == user, user_playlists) + for playlist in sort(filter(p -> canread(req[:login][:user], p), + user_playlists), + by = p -> p.name) push!(playlistsXML, playlist) end return subsonic_return(xdoc) @@ -320,14 +322,13 @@ end "Returns a listing of files in a saved playlist." function getPlaylist(req) global user_playlists - query = HTTP.URIs.queryparams(req[:query]) id = get(query, "id", "") isempty(id) && return missing_parameter("id") - - m = findfirst(x -> x.owner == req[:login][:name], user_playlists) + m = findfirst(p -> p.uuid == id, + filter(p -> canread(req[:login][:user], p), + user_playlists)) m == nothing && return not_found("id") - (xdoc, xroot) = subsonic() append!(xroot, user_playlists[m]) return subsonic_return(xroot) @@ -365,6 +366,7 @@ function updatePlaylist(req) if songIndexToRemove > 0 && songIndexToRemove <= length(playlist.songs) deleteat!(playlist.songs, songIndexToRemove) end + saveplaylists() @subsonic(nothing) end @@ -376,6 +378,7 @@ function deletePlaylist(req) isempty(id) && return missing_parameter("id") # FIXME: check ownership filter!(p -> p.uuid != id, user_playlists) + saveplaylists() @subsonic(nothing) end @@ -418,3 +421,30 @@ function sendfile(path; suffix = nothing) return Dict(:body => read(path), :headers => headers) end + +function saveplaylists(; file = expanduser("~/.config/beets/playlists.jsonl")) + global user_playlists + open(file, "w") do f + write(f, + join(JSON2.write.(user_playlists), "\n")) + end +end + +function loadplaylists(; file = expanduser("~/.config/beets/playlists.jsonl")) + global user_playlists + isfile(file) || touch(file) + ps = JSON2.readlines(file) + empty!(user_playlists) + for p in ps + # try + pl = JSON2.read(p, Playlist) + push!(user_playlists, pl) + # catch e + # @warn "Failed to read with error $e" + # isa(e, ArgumentError) && continue + # end + end +end + +canread(u::User, p::Playlist) = p.public || p.owner == u.name || u in p.allowed +canedit(u::User, p::Playlist) = p.owner == u.name diff --git a/JlSonic/beet2xml.jl b/JlSonic/beet2xml.jl index eb158e4..b423266 100644 --- a/JlSonic/beet2xml.jl +++ b/JlSonic/beet2xml.jl @@ -10,22 +10,23 @@ function push!(root::XMLElement, p::Playlist) ("public", string(p.public)), ("songCount", string(length(p.songs))), ("duration", reduce(+, p.songs, init = 0.0) |> floor |> Int |> string), - ("created", "FIXME"), + ("created", "2012-04-17T19:53:44"), ("coverArt", p.cover), ]) playlistXML - #= - sindre - john - =# end function append!(root::XMLElement, p::Playlist) playlistXML = push!(root, p) - # FIXME: 1. allowed users + # Allowed users + for al in p.allowed + set_content(new_child(playlistXML, "allowedUser"), + al) + end for song in p.songs entry = new_child(playlistXML, "entry") set_attributes(entry, props(song)) + set_attribute(entry, "artist", Beets.artist(song).name) end playlistXML end @@ -69,7 +70,7 @@ function push!(root::XMLElement, artist::Beets.Artist) ("id", artist.uuid), ("name", artist.name), ("coverArt", artist.uuid), - ("albumCount", Beets.albums(artist) |> length |> string) + ("albumCount", Beets.album(artist) |> length |> string) ]) artistXML end diff --git a/JlSonic/types.jl b/JlSonic/types.jl index eadf7a4..44176d0 100644 --- a/JlSonic/types.jl +++ b/JlSonic/types.jl @@ -1,15 +1,8 @@ -mutable struct Playlist - uuid::String - name::String - comment::String - owner::String - public::Bool - songs::Vector{Song} - cover::String -end +import Random mutable struct User name::String + password::String email::String # scrobbling::Bool admin::Bool @@ -25,17 +18,31 @@ mutable struct User share::Bool end +mutable struct Playlist + uuid::String + name::String + comment::String + owner::String + public::Bool + # FIXME: replace with uuids only (and check they exists when importing) + songs::Vector{Song} + cover::String + allowed::Vector{String} +end + function Playlist(owner::String ; uuid = string(UUIDs.uuid4()), name = "New Playlist", comment = "", public = false, songs = Song[], - cover = "",) - Playlist(uuid, name, comment, owner, public, songs, cover) + cover = "", + allowed = String[]) + Playlist(uuid, name, comment, owner, public, songs, cover, allowed) end function User(name::String) - User(name, string(name, "@", domain), + User(name, Random.randstring(30), + string(name, "@", domain), false, false, false, false, false, false, false, false, false) end diff --git a/airsonic.rest b/airsonic.rest index c24b1c6..be733e7 100644 --- a/airsonic.rest +++ b/airsonic.rest @@ -1,10 +1,10 @@ # -*- restclient -*- :url = http://localhost:8000/rest -:token = fc75eb09895ad4fe4909b81f48f9f4b4 +:token = aca7a0d5412138863f361df08e738378 :username = nixo -:salt = 12b71ql5m8l8i72g5684hu0769 -:auth = ?username=:username&t=:token&s=:salt +:salt = 2ob56atgdr8htkpdg5478c6tfj +:auth = ?u=:username&t=:token&s=:salt # Used to test connectivity with the server. Takes no extra parameters. GET :url/ping @@ -39,3 +39,11 @@ GET :url/getCoverArt:auth&id=7167f941-efef-49dd-a54f-8e2d41e3f4a7 # Stream GET :url/stream:auth&id=df5937fd-d79b-40b5-bf14-8c29c54e1bdb +# Get playlists +GET :url/getPlaylists:auth + +# Get single playlist +GET :url/getPlaylist:auth&id=512c6d5e-798f-47f7-a50d-116ef647109e + + + diff --git a/login.jl b/login.jl index 5d259b2..00e0f34 100644 --- a/login.jl +++ b/login.jl @@ -1,5 +1,9 @@ using MD5 using HTTP +using JSON2 + +const users = JlSonic.User[] + function getlogin(app, req) query = HTTP.URIs.queryparams(req[:query]) username = string(get(query, "u", "")) @@ -15,25 +19,44 @@ function getlogin(app, req) end function checkpassword(app, req) - # FIXME: do not hardcode the password here! - password = "test" - # @show bytes2hex(MD5.md5(string(password, salt))) - # @show req[:login][:token] - # @show req[:login][:salt] + global users + usern = findfirst(u -> u.name == req[:login][:name], users) + usern === nothing && return app(req) + user = users[usern] + req[:login][:user] = user if !isempty(req[:login][:salt]) - if bytes2hex(MD5.md5(string(password, req[:login][:salt]))) == + if bytes2hex(MD5.md5(string(user.password, req[:login][:salt]))) == req[:login][:token] req[:login][:login] = true end elseif !isempty(req[:login][:password]) if startswith(req[:login][:password], "enc:") req[:login][:login] = - String(hex2bytes(split(req[:login][:password], ":")[2])) == password + String(hex2bytes(split(req[:login][:password], ":")[2])) == + user.password else - req[:login][:login] = password == req[:login][:password] + req[:login][:login] = user.password == req[:login][:password] end end return app(req) end +function saveusers(file = expanduser("~/.config/beets/users.jsonl")) + global users + open(file, "w") do f + write(f, join(JSON2.write.(users), "\n")) + end +end + +function loadusers(; file = expanduser("~/.config/beets/users.jsonl")) + global users + isfile(file) || touch(file) + ps = JSON2.readlines(file) + p = JSON2.read.(ps, JlSonic.User) + empty!(users) + for pl in p + push!(users, pl) + end +end + sonic_login = stack(getlogin, checkpassword) diff --git a/server.jl b/server.jl index e050455..879880e 100644 --- a/server.jl +++ b/server.jl @@ -7,10 +7,12 @@ import Beets Beets.update_albums(); push!(LOAD_PATH, realpath("JlSonic")) using JlSonic - +JlSonic.loadplaylists() include("router.jl") include("login.jl") +loadusers() + @app sonic = ( Mux.defaults, restp("ping", _ -> ping()), @@ -24,4 +26,3 @@ if !isdefined(Main, :started) serve(sonic) started = true end -