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
-