refactor and fixes

master
nixo 2019-05-21 11:05:53 +02:00
parent 61449fd235
commit 34956871d7
9 changed files with 227 additions and 120 deletions

View File

@ -11,15 +11,18 @@ using JSON2
# The idea is to sum all the album arts in some way. But it's easier to get one random
# using FileIO, Images
const domain = "nixo.xyz"
include("types.jl")
export Playlist, Album, Artist
const domain = "nixo.xyz"
const users = User[]
const user_playlists = Vector{Playlist}()
include("api.jl")
export ping, getLicense,
# Browsing
getMusicFolders, # getIndexes, getMusicDirectory,
getMusicFolders, # getIndexes,
getMusicDirectory,
getGenres, getArtists, getArtist, getAlbum, getSong,
# Album/song list
getAlbumList, getAlbumList2, getRandomSongs,
@ -42,5 +45,6 @@ export auth_failed
include("beethelpers.jl")
include("beet2xml.jl")
include("login.jl")
end # module JlSonic

View File

@ -63,7 +63,88 @@ end
# Implement:
# getIndexes
# getMusicDirectory
function getMusicDirectory(req)
query = HTTP.URIs.queryparams(req[:query])
id = get(query, "id", "")
isempty(id) && return missing_parameter()
(xdoc, xroot) = subsonic(version = "1.0.0")
directory = new_child(xroot, "directory")
# We simulate directory listing. Root directory has id ==
# 1. Other directories are identified by uuids
artists = Beets.getartists()
## Structure is: Music(id=1)/Artist/Album/Song
if id == "1"
# List artists
for artist in artists
content = new_child(directory, "child")
set_attributes(content,
[("id", artist.uuid),
# Always under /Music, id = 1
("parent", "1"),
("name", artist.name),
# ("starred", "FIXME")
])
end
else
# List content
# 1. Search if uuid matches artist
# 2. Else, check if matches albums
artistmatch = findfirst(a -> a.uuid == id, artists)
albums = Beets.getalbums();
if artistmatch != nothing
@show id
artist = artists[artistmatch]
set_attributes(directory,
[("id", artist.uuid),
# Always under /Music, id = 1
("parent", "1"),
("name", artist.name),
("starred", "2013-11-02T12:30:00")
])
# List albums
content = new_child(directory, "child")
for albumn in findall(alb -> alb.artist == artist, albums)
album = albums[albumn]
set_attributes(content, [
("id", album.uuid),
("parent", album.artist.uuid),
# ("artistId", album.artist.uuid),
("title", album.title),
("artist", album.artist.name),
("isDir", "true"),
("coverArt", album.uuid),
])
end
elseif false
content = new_child(directory, "child")
# List album content (songs)
set_attributes(content, [
("id", "FIXME"),
("parent", "PARENT:ID"),
("title", "FIXME"),
("isDir", "false"),
("album", "FIXME"),
("artist", "FIXME"),
("track", "FIXME"),
("year", "FIXME"),
("genre", "FIXME"),
("coverArt", "FIXME"),
("size", "FIXME"),
# FIXME
("contentType", "audio/mpeg"),
("suffix", "FIXME"),
("duration", "FIXME"),
("bitrate", "FIXME"),
("path", "FIXME"),
])
else
return not_found()
end
end
doc_str = string(xdoc)
free(xdoc)
return doc_str
end
"Returns all genres."
function getGenres()
@ -98,7 +179,7 @@ function getArtists()
for index in string.(firstletters)
indexXML = new_child(indexes, "index")
set_attribute(indexXML, "name", index)
for artist in unique(filter(x -> startswith(x.name, index), artists))
for artist in filter(x -> startswith(x.name, index), artists)
artistXML = push!(indexXML, artist)
end
end
@ -109,7 +190,7 @@ end
This method organizes music according to ID3 tags."""
function getArtist(id::String)
artists = Beets.getartists()
artists = Beets.artists()
matching = findfirst(a -> a.uuid == id, artists)
matching === nothing && return not_found("id")
artist = artists[matching]
@ -264,8 +345,6 @@ function search3(req)
return subsonic_return(xdoc)
end
const user_playlists = Vector{Playlist}()
"Create (or update) a playlist" # WTF create can update?
function createPlaylist(req)
global user_playlists
@ -293,10 +372,10 @@ function createPlaylist(req)
end
elseif !isempty(name)
playlist = Playlist(req[:login][:user].name,
name = name,
# cover = ???
name = name # cover = ???
)
push!(playlist, song)
@show "THERE"
else
return missing_parameter("either name or playlistId")
end
@ -323,11 +402,11 @@ function getPlaylists(req)
end
import Base.get
function get(::Type{Playlist}, u::User, id::AbstractString)
function get(::Type{Playlist}, u::User, id::AbstractString)::Union{Nothing,Playlist}
global user_playlists
findfirst(p -> p.uuid == id,
filter(p -> canread(u, p),
user_playlists))
playlists = filter(p -> canread(u, p), user_playlists)
m = findfirst(p -> p.uuid == id, playlists)
m == nothing ? nothing : playlists[m]
end
"Returns a listing of files in a saved playlist."
@ -336,10 +415,10 @@ function getPlaylist(req)
query = HTTP.URIs.queryparams(req[:query])
id = get(query, "id", "")
isempty(id) && return missing_parameter("id")
m = get(Playlist, req[:login][:user], id)
m == nothing && return not_found("id")
playlist = get(Playlist, req[:login][:user], id)
playlist == nothing && return not_found("id")
(xdoc, xroot) = subsonic()
append!(xroot, user_playlists[m])
append!(xroot, playlist)
return subsonic_return(xroot)
end
@ -349,16 +428,26 @@ function updatePlaylist(req)
query = HTTP.URIs.queryparams(req[:query])
playlistId = get(query, "playlistId", "")
isempty(playlistId) && return missing_parameter("playlistId")
m = get(Playlist, req[:login][:user], playlistId)
m == nothing && return not_found("playlistId")
playlist = user_playlists[m]
playlist = get(Playlist, req[:login][:user], playlistId)
playlist == nothing && return not_found("playlistId")
# Check ownership (if not allowed, should not even reach this (canread is false))
canedit(req[:login][:user], playlist) || return not_allowed()
canedit(req[:login][:user], playlist) || return unuthorized()
# Want to make public. Is allowed?
wantpublic = try
parse(Bool,get(query, "public", string(playlist.public)))
catch e
isa(e, ArgumentError) ? false : @error e
end
if wantpublic
canmakepublic(req[:login][:user]) || return unuthorized()
playlist.public = true
else
playlist.public = false
end
playlist.name = get(query, "name", playlist.name)
playlist.comment = get(query, "comment", playlist.comment)
# FIXME: use try/catch
playlist.public = parse(Bool,get(query, "public", string(playlist.public)))
songIdAdd = get(query, "songIdToAdd", "")
# WTF by the index!?
IndexToRemove = get(query, "songIndexToRemove", "")
@ -416,9 +505,11 @@ function getCoverArt(req::Dict)
return sendfile(Beets.albums[n].cover)
end
canstream(u::User) = u.stream
"Streams a given media file."
function stream(req::Dict)
query = HTTP.URIs.queryparams(req[:query])
canstream(req[:login][:user]) || return unuthorized()
id = get(query, "id", "")
isempty(id) && return missing_parameter("id")
songs = Beets.songs()
@ -432,35 +523,20 @@ 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]
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
headers["Content-Type"] = mime
headers["Content-Length"] = string(filesize(path))
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
canread(u::User, p::Playlist) = p.public ||
(u.admin || p.owner == u.name) ||
u in p.allowed
function canedit(u::User, p::Playlist)
@show p.owner
@show u.name
@show u.admin
(p.owner == u.name) || u.admin
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
canmakepublic(u::User) = u.playlist

View File

@ -1,5 +1,12 @@
import Dates
ms2string(m::Dates.DateTime) = Dates.format(m, Dates.dateformat"YYYY-mm-ddTHH:MM:SS")
import Base.push!
# Try to fix missing mime types
Mux.mimetypes["alac"] = "audio/x-m4a"
Mux.mimetypes["m4a"] = "audio/x-m4a"
function push!(root::XMLElement, p::Playlist)
playlistXML = new_child(root, "playlist")
set_attributes(playlistXML, [
@ -10,7 +17,7 @@ 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", "2012-04-17T19:53:44"),
("created", ms2string(p.created)),
("coverArt", p.cover),
])
playlistXML
@ -20,8 +27,7 @@ function append!(root::XMLElement, p::Playlist)
playlistXML = push!(root, p)
# Allowed users
for al in p.allowed
set_content(new_child(playlistXML, "allowedUser"),
al)
set_content(new_child(playlistXML, "allowedUser"), al)
end
for song in p.songs
entry = new_child(playlistXML, "entry")
@ -40,7 +46,7 @@ function push!(root::XMLElement, album::Beets.Album)
("name", album.title),
("coverArt", album.uuid),
("songCount", string(length(album.songs))),
("created", "0"), # FIXME
("created", ms2string(album.added)),
("duration", string(sum([t.length for t in album.songs]) |> floor |> Int)),
("artist", album.artist.name),
("artistId", album.artist.uuid)
@ -78,16 +84,17 @@ end
function push!(root::XMLElement, song::Beets.Song)
songXML = new_child(root, "song")
suffix = lowercase(song.format)
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
set_attributes(songXML, [
("id", song.uuid),
("title", song.title),
("isDir", "false"),
("created", "FIXME"),
("created", ms2string(song.added)),
("duration", string(floor(song.length) |> Int)),
("bitrate", string(song.bitrate)),
("size", string(filesize(song.path))),
("suffix", suffix),
("contentType", Mux.mimetypes[suffix]),
("contentType", mime),
("isVideo", "false"),
("path", relpath(song.path, Beets.musicdir())),
("type", "music")
@ -99,6 +106,7 @@ function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album})
songXML = new_child(root, "song")
song, album = song_album
suffix = lowercase(song.format)
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
set_attributes(songXML, [
("id", song.uuid),
("parent", album.artist.uuid), # Not clear
@ -107,12 +115,12 @@ function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album})
("artist", album.artist.name),
("isDir", "false"),
("coverArt", album.uuid),
("created", "FIXME"),
("created", ms2string(album.added)),
("duration", string(floor(song.length) |> Int)),
("bitrate", string(song.bitrate)),
("size", string(filesize(song.path))),
("suffix", suffix),
("contentType", Mux.mimetypes[suffix]), # mpeg
("contentType", mime),
("isVideo", "false"),
("path", relpath(song.path, Beets.musicdir())),
("albumId", album.uuid),
@ -124,6 +132,7 @@ end
function props(song::Song)
suffix = lowercase(song.format)
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
[
("id", song.uuid),
# ("parent", album.artist.uuid), # Not clear
@ -132,12 +141,12 @@ function props(song::Song)
# ("artist", song.album.artist.name),
("isDir", "false"),
("coverArt", song.uuid),
("created", "FIXME"),
("created", ms2string(song.added)),
("duration", string(floor(song.length) |> Int)),
("bitrate", string(song.bitrate)),
("size", string(filesize(song.path))),
("suffix", suffix),
("contentType", Mux.mimetypes[suffix]), # mpeg
("contentType", mime), # mpeg
("isVideo", "false"),
("path", relpath(song.path, Beets.musicdir())),
# ("albumId", song.album.uuid),

View File

@ -1,4 +1,44 @@
function allsongs()
albums = [album.songs for album in Beets.getalbums()];
songs = Iterators.flatten(albums) |> collect;
playlistfile(path) = joinpath(path, "playlists.jsonl")
function saveplaylists(; file = playlistfile(Beets.confdir()))
global user_playlists
open(file, "w") do f
write(f,
join(JSON2.write.(user_playlists), "\n"))
end
end
function loadplaylists(; file = playlistfile(Beets.confdir()))
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
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

View File

@ -1,5 +1,5 @@
import Random
import Dates
mutable struct User
name::String
password::String
@ -28,6 +28,7 @@ mutable struct Playlist
songs::Vector{Song}
cover::String
allowed::Vector{String}
created::Dates.DateTime
end
function Playlist(owner::String
@ -37,8 +38,12 @@ function Playlist(owner::String
public = false,
songs = Song[],
cover = "",
allowed = String[])
Playlist(uuid, name, comment, owner, public, songs, cover, allowed)
allowed = String[],
creation = Dates.now())
Playlist(uuid,
name, comment, owner, public,
songs, cover,
allowed, creation)
end
function User(name::String)

View File

@ -15,8 +15,8 @@ GET :url/getLicense
# Returns all configured top-level music folders. Takes no extra parameters.
GET :url/getMusicFolders:auth
# getIndexes = Returns an indexed structure of all artists.
# getMusicDirectory = Returns a listing of all files in a music directory. Typically used to get list of albums for an artist, or list of songs for an album.
#
GET :url/getMusicDirectory:auth&id=fab34286-b8e1-4879-bce3-194e1358fbd2
# Returns all genres.
GET :url/getGenres:auth
@ -24,26 +24,27 @@ GET :url/getGenres:auth
# Similar to getIndexes, but organizes music according to ID3 tags.
GET :url/getArtists:auth
# Returns details for an artist, including a list of albums. This method organizes music according to ID3 tags.
GET :url/getArtist:auth&id=14d44067-99c2-4f77-b58b-138f0b6911fa
# Returns details for an artist, including a list of albums. This method organizes music according to ID3 tags.14d44067-99c2-4f77-b58b-138f0b6911fa
GET :url/getArtist:auth&id=ba853904-ae25-4ebb-89d6-c44cfbd71bd2
# Returns details for an album, including a list of songs. This method organizes music according to ID3 tags.
GET :url/getAlbum:auth&id=d9522a40-887f-4a15-a59f-0d3bccfa908f
GET :url/getAlbum:auth&id=f281e63f-589d-4691-8f13-9906ccc09aa0
# Returns details for a song.
GET :url/getSong:auth&id=df5937fd-d79b-40b5-bf14-8c29c54e1bdb
GET :url/getSong:auth&id=e1ebe027-2e21-45c9-bff8-94ba538f895f
# Returns a cover art image.
GET :url/getCoverArt:auth&id=7167f941-efef-49dd-a54f-8e2d41e3f4a7
# Stream
GET :url/stream:auth&id=df5937fd-d79b-40b5-bf14-8c29c54e1bdb
GET :url/stream:auth&id=e1ebe027-2e21-45c9-bff8-94ba538f895f
# Get playlists
GET :url/getPlaylists:auth
# Get single playlist
GET :url/getPlaylist:auth&id=a2df9320-4775-40a5-9830-8960f3eb9203
GET :url/getPlaylist:auth&id=1455e415-8718-4453-a5f5-490a00b62d34
# Get not owned playlist
GET :url/getPlaylist:auth&id=799f5074-5db2-4daa-b449-9677d0c7744c
@ -52,7 +53,7 @@ GET :url/getPlaylist:auth&id=799f5074-5db2-4daa-b449-9677d0c7744c
GET :url/deletePlaylist:auth&id=799f5074-5db2-4daa-b449-9677d0c7744c
# Update not owned playlist
GET :url/updatePlaylist:auth&playlistId=799f5074-5db2-4daa-b449-9677d0c7744c
GET :url/updatePlaylist:auth&playlistId=e39d8798-473e-45a9-8a1f-d5d0485ed274
# Update owned playlist
GET :url/updatePlaylist:auth&playlistId=a2df9320-4775-40a5-9830-8960f3eb9203&name=nuovo
@ -60,5 +61,8 @@ GET :url/updatePlaylist:auth&playlistId=a2df9320-4775-40a5-9830-8960f3eb9203&nam
# Delete owned playlist
GET :url/deletePlaylist:auth&id=a2df9320-4775-40a5-9830-8960f3eb9203
# Get random songs
GET :url/getRandomSongs:auth

View File

@ -2,59 +2,28 @@ 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", ""))
token = get(query, "t", "")
salt = get(query, "s", "")
password = get(query, "p", "")
req[:login] = Dict(:name => username,
:token => token,
:salt => salt,
:password => password,
req[:login] = Dict(:name => string(get(query, "u", "")),
:token => get(query, "t", ""),
:salt => get(query, "s", ""),
:password => get(query, "p", ""),
:login => false)
return app(req)
end
function checkpassword(app, req)
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(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])) ==
user.password
else
req[:login][:login] = user.password == req[:login][:password]
end
user = JlSonic.checkpass(req[:login][:name],
req[:login][:salt],
req[:login][:token],
req[:login][:password])
if user == nothing
req[:login][:login] = false
else
req[:login][:login] = true
req[:login][:user] = user
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)
append!(users, p)
end
sonic_login = stack(getlogin, checkpassword)

View File

@ -9,7 +9,7 @@ restp(p, app...) = branch(req -> restpath!(p, req), app...)
dispatch = stack(
# Browsing
restp("getMusicFolders", _ -> getMusicFolders()),
restp("getMusicDirectory", req -> getmusicdirectory(req)),
restp("getMusicDirectory", req -> getMusicDirectory(req)),
restp("getAlbumList", req -> getAlbumList(req)),
restp("getGenres", _ -> getGenres()),
restp("getArtists", _ -> getArtists()),

View File

@ -8,10 +8,10 @@ Beets.update_albums();
push!(LOAD_PATH, realpath("JlSonic"))
using JlSonic
JlSonic.loadplaylists()
JlSonic.loadusers()
include("router.jl")
include("login.jl")
loadusers()
@app sonic = (
Mux.defaults,