allow saving users and playlists
This commit is contained in:
parent
77f18ab080
commit
c7a7dbd7fa
|
@ -5,6 +5,7 @@ using Beets
|
||||||
using LightXML
|
using LightXML
|
||||||
import UUIDs
|
import UUIDs
|
||||||
import HTTP
|
import HTTP
|
||||||
|
using JSON2
|
||||||
|
|
||||||
const domain = "nixo.xyz"
|
const domain = "nixo.xyz"
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ function getArtist(id::String)
|
||||||
# Create the response
|
# Create the response
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
artistXML = push!(xroot, artist)
|
artistXML = push!(xroot, artist)
|
||||||
for album in Beets.albums(artist)
|
for album in Beets.album(artist)
|
||||||
push!(artistXML, album)
|
push!(artistXML, album)
|
||||||
end
|
end
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
|
@ -292,7 +292,7 @@ function createPlaylist(req)
|
||||||
return not_found("playlistId")
|
return not_found("playlistId")
|
||||||
end
|
end
|
||||||
elseif !isempty(name)
|
elseif !isempty(name)
|
||||||
playlist = Playlist(req[:login][:name], name = name)
|
playlist = Playlist(req[:login][:user].name, name = name)
|
||||||
push!(playlist, song)
|
push!(playlist, song)
|
||||||
else
|
else
|
||||||
return missing_parameter("either name or playlistId")
|
return missing_parameter("either name or playlistId")
|
||||||
|
@ -301,17 +301,19 @@ function createPlaylist(req)
|
||||||
# Return the playlist
|
# Return the playlist
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
push!(xroot, playlist)
|
push!(xroot, playlist)
|
||||||
|
saveplaylists()
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
end
|
end
|
||||||
|
|
||||||
"Returns all playlists a user is allowed to play."
|
"Returns all playlists a user is allowed to play."
|
||||||
function getPlaylists(req)
|
function getPlaylists(req)
|
||||||
global user_playlists
|
global user_playlists
|
||||||
user = req[:login][:name]
|
|
||||||
# FIXME: add support for admin (ask other user's playlists, v 1.8.0)
|
# FIXME: add support for admin (ask other user's playlists, v 1.8.0)
|
||||||
(xdoc, xroot) = subsonic(version = "1.0.0")
|
(xdoc, xroot) = subsonic(version = "1.0.0")
|
||||||
playlistsXML = new_child(xroot, "playlists")
|
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)
|
push!(playlistsXML, playlist)
|
||||||
end
|
end
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
|
@ -320,14 +322,13 @@ end
|
||||||
"Returns a listing of files in a saved playlist."
|
"Returns a listing of files in a saved playlist."
|
||||||
function getPlaylist(req)
|
function getPlaylist(req)
|
||||||
global user_playlists
|
global user_playlists
|
||||||
|
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
id = get(query, "id", "")
|
id = get(query, "id", "")
|
||||||
isempty(id) && return missing_parameter("id")
|
isempty(id) && return missing_parameter("id")
|
||||||
|
m = findfirst(p -> p.uuid == id,
|
||||||
m = findfirst(x -> x.owner == req[:login][:name], user_playlists)
|
filter(p -> canread(req[:login][:user], p),
|
||||||
|
user_playlists))
|
||||||
m == nothing && return not_found("id")
|
m == nothing && return not_found("id")
|
||||||
|
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
append!(xroot, user_playlists[m])
|
append!(xroot, user_playlists[m])
|
||||||
return subsonic_return(xroot)
|
return subsonic_return(xroot)
|
||||||
|
@ -365,6 +366,7 @@ function updatePlaylist(req)
|
||||||
if songIndexToRemove > 0 && songIndexToRemove <= length(playlist.songs)
|
if songIndexToRemove > 0 && songIndexToRemove <= length(playlist.songs)
|
||||||
deleteat!(playlist.songs, songIndexToRemove)
|
deleteat!(playlist.songs, songIndexToRemove)
|
||||||
end
|
end
|
||||||
|
saveplaylists()
|
||||||
@subsonic(nothing)
|
@subsonic(nothing)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -376,6 +378,7 @@ function deletePlaylist(req)
|
||||||
isempty(id) && return missing_parameter("id")
|
isempty(id) && return missing_parameter("id")
|
||||||
# FIXME: check ownership
|
# FIXME: check ownership
|
||||||
filter!(p -> p.uuid != id, user_playlists)
|
filter!(p -> p.uuid != id, user_playlists)
|
||||||
|
saveplaylists()
|
||||||
@subsonic(nothing)
|
@subsonic(nothing)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -418,3 +421,30 @@ function sendfile(path; suffix = nothing)
|
||||||
return Dict(:body => read(path),
|
return Dict(:body => read(path),
|
||||||
:headers => headers)
|
:headers => headers)
|
||||||
end
|
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
|
||||||
|
|
|
@ -10,22 +10,23 @@ function push!(root::XMLElement, p::Playlist)
|
||||||
("public", string(p.public)),
|
("public", string(p.public)),
|
||||||
("songCount", string(length(p.songs))),
|
("songCount", string(length(p.songs))),
|
||||||
("duration", reduce(+, p.songs, init = 0.0) |> floor |> Int |> string),
|
("duration", reduce(+, p.songs, init = 0.0) |> floor |> Int |> string),
|
||||||
("created", "FIXME"),
|
("created", "2012-04-17T19:53:44"),
|
||||||
("coverArt", p.cover),
|
("coverArt", p.cover),
|
||||||
])
|
])
|
||||||
playlistXML
|
playlistXML
|
||||||
#=
|
|
||||||
<allowedUser>sindre</allowedUser>
|
|
||||||
<allowedUser>john</allowedUser>
|
|
||||||
=#
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function append!(root::XMLElement, p::Playlist)
|
function append!(root::XMLElement, p::Playlist)
|
||||||
playlistXML = push!(root, p)
|
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
|
for song in p.songs
|
||||||
entry = new_child(playlistXML, "entry")
|
entry = new_child(playlistXML, "entry")
|
||||||
set_attributes(entry, props(song))
|
set_attributes(entry, props(song))
|
||||||
|
set_attribute(entry, "artist", Beets.artist(song).name)
|
||||||
end
|
end
|
||||||
playlistXML
|
playlistXML
|
||||||
end
|
end
|
||||||
|
@ -69,7 +70,7 @@ function push!(root::XMLElement, artist::Beets.Artist)
|
||||||
("id", artist.uuid),
|
("id", artist.uuid),
|
||||||
("name", artist.name),
|
("name", artist.name),
|
||||||
("coverArt", artist.uuid),
|
("coverArt", artist.uuid),
|
||||||
("albumCount", Beets.albums(artist) |> length |> string)
|
("albumCount", Beets.album(artist) |> length |> string)
|
||||||
])
|
])
|
||||||
artistXML
|
artistXML
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
mutable struct Playlist
|
import Random
|
||||||
uuid::String
|
|
||||||
name::String
|
|
||||||
comment::String
|
|
||||||
owner::String
|
|
||||||
public::Bool
|
|
||||||
songs::Vector{Song}
|
|
||||||
cover::String
|
|
||||||
end
|
|
||||||
|
|
||||||
mutable struct User
|
mutable struct User
|
||||||
name::String
|
name::String
|
||||||
|
password::String
|
||||||
email::String
|
email::String
|
||||||
# scrobbling::Bool
|
# scrobbling::Bool
|
||||||
admin::Bool
|
admin::Bool
|
||||||
|
@ -25,17 +18,31 @@ mutable struct User
|
||||||
share::Bool
|
share::Bool
|
||||||
end
|
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
|
function Playlist(owner::String
|
||||||
; uuid = string(UUIDs.uuid4()),
|
; uuid = string(UUIDs.uuid4()),
|
||||||
name = "New Playlist",
|
name = "New Playlist",
|
||||||
comment = "",
|
comment = "",
|
||||||
public = false,
|
public = false,
|
||||||
songs = Song[],
|
songs = Song[],
|
||||||
cover = "",)
|
cover = "",
|
||||||
Playlist(uuid, name, comment, owner, public, songs, cover)
|
allowed = String[])
|
||||||
|
Playlist(uuid, name, comment, owner, public, songs, cover, allowed)
|
||||||
end
|
end
|
||||||
|
|
||||||
function User(name::String)
|
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)
|
false, false, false, false, false, false, false, false, false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# -*- restclient -*-
|
# -*- restclient -*-
|
||||||
|
|
||||||
:url = http://localhost:8000/rest
|
:url = http://localhost:8000/rest
|
||||||
:token = fc75eb09895ad4fe4909b81f48f9f4b4
|
:token = aca7a0d5412138863f361df08e738378
|
||||||
:username = nixo
|
:username = nixo
|
||||||
:salt = 12b71ql5m8l8i72g5684hu0769
|
:salt = 2ob56atgdr8htkpdg5478c6tfj
|
||||||
:auth = ?username=:username&t=:token&s=:salt
|
:auth = ?u=:username&t=:token&s=:salt
|
||||||
|
|
||||||
# Used to test connectivity with the server. Takes no extra parameters.
|
# Used to test connectivity with the server. Takes no extra parameters.
|
||||||
GET :url/ping
|
GET :url/ping
|
||||||
|
@ -39,3 +39,11 @@ GET :url/getCoverArt:auth&id=7167f941-efef-49dd-a54f-8e2d41e3f4a7
|
||||||
# Stream
|
# Stream
|
||||||
GET :url/stream:auth&id=df5937fd-d79b-40b5-bf14-8c29c54e1bdb
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
39
login.jl
39
login.jl
|
@ -1,5 +1,9 @@
|
||||||
using MD5
|
using MD5
|
||||||
using HTTP
|
using HTTP
|
||||||
|
using JSON2
|
||||||
|
|
||||||
|
const users = JlSonic.User[]
|
||||||
|
|
||||||
function getlogin(app, req)
|
function getlogin(app, req)
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
username = string(get(query, "u", ""))
|
username = string(get(query, "u", ""))
|
||||||
|
@ -15,25 +19,44 @@ function getlogin(app, req)
|
||||||
end
|
end
|
||||||
|
|
||||||
function checkpassword(app, req)
|
function checkpassword(app, req)
|
||||||
# FIXME: do not hardcode the password here!
|
global users
|
||||||
password = "test"
|
usern = findfirst(u -> u.name == req[:login][:name], users)
|
||||||
# @show bytes2hex(MD5.md5(string(password, salt)))
|
usern === nothing && return app(req)
|
||||||
# @show req[:login][:token]
|
user = users[usern]
|
||||||
# @show req[:login][:salt]
|
req[:login][:user] = user
|
||||||
if !isempty(req[:login][:salt])
|
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][:token]
|
||||||
req[:login][:login] = true
|
req[:login][:login] = true
|
||||||
end
|
end
|
||||||
elseif !isempty(req[:login][:password])
|
elseif !isempty(req[:login][:password])
|
||||||
if startswith(req[:login][:password], "enc:")
|
if startswith(req[:login][:password], "enc:")
|
||||||
req[:login][:login] =
|
req[:login][:login] =
|
||||||
String(hex2bytes(split(req[:login][:password], ":")[2])) == password
|
String(hex2bytes(split(req[:login][:password], ":")[2])) ==
|
||||||
|
user.password
|
||||||
else
|
else
|
||||||
req[:login][:login] = password == req[:login][:password]
|
req[:login][:login] = user.password == req[:login][:password]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return app(req)
|
return app(req)
|
||||||
end
|
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)
|
sonic_login = stack(getlogin, checkpassword)
|
||||||
|
|
|
@ -7,10 +7,12 @@ import Beets
|
||||||
Beets.update_albums();
|
Beets.update_albums();
|
||||||
push!(LOAD_PATH, realpath("JlSonic"))
|
push!(LOAD_PATH, realpath("JlSonic"))
|
||||||
using JlSonic
|
using JlSonic
|
||||||
|
JlSonic.loadplaylists()
|
||||||
|
|
||||||
include("router.jl")
|
include("router.jl")
|
||||||
include("login.jl")
|
include("login.jl")
|
||||||
|
loadusers()
|
||||||
|
|
||||||
@app sonic = (
|
@app sonic = (
|
||||||
Mux.defaults,
|
Mux.defaults,
|
||||||
restp("ping", _ -> ping()),
|
restp("ping", _ -> ping()),
|
||||||
|
@ -24,4 +26,3 @@ if !isdefined(Main, :started)
|
||||||
serve(sonic)
|
serve(sonic)
|
||||||
started = true
|
started = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue