allow shares
parent
a87081746d
commit
d180c207d8
|
@ -6,6 +6,7 @@ using LightXML
|
||||||
import UUIDs
|
import UUIDs
|
||||||
import HTTP
|
import HTTP
|
||||||
using JSON2
|
using JSON2
|
||||||
|
import Dates
|
||||||
|
|
||||||
# # Playlist cover art support
|
# # Playlist cover art support
|
||||||
# The idea is to sum all the album arts in some way. But it's easier to get one random
|
# The idea is to sum all the album arts in some way. But it's easier to get one random
|
||||||
|
@ -17,28 +18,37 @@ export Playlist, Album, Artist
|
||||||
const domain = "nixo.xyz"
|
const domain = "nixo.xyz"
|
||||||
const users = User[]
|
const users = User[]
|
||||||
const user_playlists = Vector{Playlist}()
|
const user_playlists = Vector{Playlist}()
|
||||||
|
# user => share
|
||||||
|
const shared = Vector{Share}()
|
||||||
|
|
||||||
|
# map username => (starred element, starred date)
|
||||||
|
user_stars = Dict{String,Vector{Star}}()
|
||||||
|
user_ratings = Dict{String, Vector{Rating}}()
|
||||||
|
user_playqueue = Dict{String, Vector{String}}()
|
||||||
|
|
||||||
include("api.jl")
|
include("api.jl")
|
||||||
export ping, getLicense,
|
export ping, getLicense,
|
||||||
# Browsing
|
# Browsing
|
||||||
getMusicFolders, # getIndexes,
|
getMusicFolders, # getIndexes,
|
||||||
getMusicDirectory,
|
getMusicDirectory, getShares, createShare, updateShare, deleteShare,
|
||||||
getGenres, getArtists, getArtist, getAlbum, getSong,
|
getGenres, getArtists, getArtist, getAlbum, getSong, getLyrics,
|
||||||
# Album/song list
|
# Album/song list
|
||||||
getAlbumList, getAlbumList2, getRandomSongs,
|
getAlbumList, getAlbumList2, getRandomSongs,
|
||||||
getSongsByGenre, getNowPlaying, getStarred, getStarred2,
|
getSongsByGenre, getNowPlaying, getStarred, getStarred2,
|
||||||
|
star, unstar, setRating,
|
||||||
# Searching
|
# Searching
|
||||||
search3,
|
search3,
|
||||||
# Playlists
|
# Playlists
|
||||||
getPlaylists, getPlaylist, createPlaylist,
|
getPlaylists, getPlaylist, createPlaylist,
|
||||||
updatePlaylist, deletePlaylist,
|
updatePlaylist, deletePlaylist,
|
||||||
|
savePlayQueue,
|
||||||
# Media retrieval
|
# Media retrieval
|
||||||
stream,
|
stream,
|
||||||
# download, hls, getCaptions,
|
# download, hls, getCaptions,
|
||||||
getCoverArt, # getLyrics, getAvatar,
|
getCoverArt, # getLyrics, getAvatar,
|
||||||
|
scrobble,
|
||||||
# User management
|
# User management
|
||||||
getUser # getUsers, createUser, updateUser, deleteUser, changePassword
|
getUser, getUsers , createUser, updateUser, deleteUser, changePassword
|
||||||
|
|
||||||
|
|
||||||
include("errors.jl")
|
include("errors.jl")
|
||||||
export auth_failed
|
export auth_failed
|
||||||
|
|
567
JlSonic/api.jl
567
JlSonic/api.jl
|
@ -2,14 +2,16 @@
|
||||||
Implementation of Subsonic APIs: http://www.subsonic.org/pages/api.jsp
|
Implementation of Subsonic APIs: http://www.subsonic.org/pages/api.jsp
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import Humanize: datasize
|
||||||
|
|
||||||
"Subsonic API compatible version"
|
"Subsonic API compatible version"
|
||||||
const apiversion = "1.16.1"
|
const apiversion = "4.5"
|
||||||
const domain = "nixo.xyz"
|
const domain = "nixo.xyz"
|
||||||
const ffmpeg_threads = 0
|
const ffmpeg_threads = 0
|
||||||
"""
|
"""
|
||||||
Helper function that prepares a subsonic response.
|
Helper function that prepares a subsonic response.
|
||||||
"""
|
"""
|
||||||
function subsonic(; version = "1.10.1", status = "ok")
|
function subsonic(; version = apiversion, status = "ok")
|
||||||
xdoc = XMLDocument()
|
xdoc = XMLDocument()
|
||||||
xroot = create_root(xdoc, "subsonic-response")
|
xroot = create_root(xdoc, "subsonic-response")
|
||||||
set_attribute(xroot, "xmlns", "http://subsonic.org/restapi")
|
set_attribute(xroot, "xmlns", "http://subsonic.org/restapi")
|
||||||
|
@ -67,7 +69,7 @@ function getMusicDirectory(req)
|
||||||
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()
|
isempty(id) && return missing_parameter()
|
||||||
(xdoc, xroot) = subsonic(version = "1.0.0")
|
(xdoc, xroot) = subsonic()
|
||||||
directory = new_child(xroot, "directory")
|
directory = new_child(xroot, "directory")
|
||||||
# We simulate directory listing. Root directory has id ==
|
# We simulate directory listing. Root directory has id ==
|
||||||
# 1. Other directories are identified by uuids
|
# 1. Other directories are identified by uuids
|
||||||
|
@ -92,7 +94,6 @@ function getMusicDirectory(req)
|
||||||
artistmatch = findfirst(a -> a.uuid == id, artists)
|
artistmatch = findfirst(a -> a.uuid == id, artists)
|
||||||
albums = Beets.albums;
|
albums = Beets.albums;
|
||||||
if artistmatch != nothing
|
if artistmatch != nothing
|
||||||
@show id
|
|
||||||
artist = artists[artistmatch]
|
artist = artists[artistmatch]
|
||||||
set_attributes(directory,
|
set_attributes(directory,
|
||||||
[("id", artist.uuid),
|
[("id", artist.uuid),
|
||||||
|
@ -226,16 +227,51 @@ function getAlbum(albumid)
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: add getStarred2
|
||||||
function getAlbumList(req::Dict)
|
function getAlbumList(req::Dict)
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
albumtype = get(query, "type", "")
|
albumtype = get(query, "type", "")
|
||||||
isempty(albumtype) && return missing_parameter("type")
|
isempty(albumtype) && return missing_parameter("type")
|
||||||
@subsonic begin
|
# The number of albums to return. Max 500.
|
||||||
list = new_child(xroot, "albumList")
|
size = abs(min(parse(Int, get(query, "size", "500")), 500))
|
||||||
push!.(Ref(list), Beets.albums)
|
offset = parse(Int, get(query, "offset", "0"))
|
||||||
|
offset = min(max(abs(offset), 0) + 1, length(Beets.albums))
|
||||||
|
tot = length(Beets.albums)
|
||||||
|
# Sort
|
||||||
|
# random, newest, highest, frequent, recent
|
||||||
|
# alphabeticalByName, alphabeticalByArtist
|
||||||
|
# starred, byYear, byGenre
|
||||||
|
asked_range = offset:min(tot,min(offset+size,tot))
|
||||||
|
if albumtype == "newest"
|
||||||
|
albums = sort!(Beets.albums, by = x -> x.added, rev = true)
|
||||||
|
albums = albums[asked_range]
|
||||||
|
elseif albumtype == "random"
|
||||||
|
albums = Beets.albums[Random.randperm(length(Beets.albums))][asked_range]
|
||||||
|
elseif albumtype == "alphabeticalByName"
|
||||||
|
albums = sort!(Beets.albums, by = x -> x.title)
|
||||||
|
albums = albums[asked_range]
|
||||||
|
elseif albumtype == "byGenre"
|
||||||
|
# Required
|
||||||
|
genre = get(query, "genre", missing)
|
||||||
|
isempty(genre) && return missing_parameter("genre")
|
||||||
|
genre = strip(genre)
|
||||||
|
albums = filter(x -> begin
|
||||||
|
length(x.songs) > 0 ? any(strip.(getfield.(x.songs,:genre)) .== genre) : false
|
||||||
|
end,
|
||||||
|
Beets.albums)
|
||||||
|
# @show length(albums)
|
||||||
|
else
|
||||||
|
albums = Beets.albums
|
||||||
end
|
end
|
||||||
|
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
list = new_child(xroot, "albumList")
|
||||||
|
push!.(Ref(list), albums)
|
||||||
|
return subsonic_return(xdoc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
getAlbumList2(req::Dict) = getAlbumList(req)
|
||||||
|
|
||||||
function getSong(req)
|
function getSong(req)
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
id = get(query, "id", "")
|
id = get(query, "id", "")
|
||||||
|
@ -249,6 +285,41 @@ function getSong(req)
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function getLyrics(req)
|
||||||
|
# I really hate those API
|
||||||
|
# artist No The artist name.
|
||||||
|
# title No The song title.
|
||||||
|
# So one can look for a lyric without artist and title? what if we have many results?
|
||||||
|
# Why was not ok to use the id?
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
artist = unescape(get(query, "artist", ""))
|
||||||
|
title = unescape(get(query, "title", ""))
|
||||||
|
if isempty(artist) && isempty(album)
|
||||||
|
return missing_parameter("At least title or artist is required")
|
||||||
|
end
|
||||||
|
if isempty(title)
|
||||||
|
# Should I return all lyrics for this artist? I refuse to do so
|
||||||
|
return missing_parameter("give me a title too, please")
|
||||||
|
end
|
||||||
|
artists = Beets.artists()
|
||||||
|
n = findall(x -> x.artist.name == artist, Beets.albums)
|
||||||
|
for a in n
|
||||||
|
for s in Beets.albums[a].songs
|
||||||
|
if s.title == title
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
el = new_child(xroot, "lyrics")
|
||||||
|
set_attribute(el, "title", title)
|
||||||
|
set_attribute(el, "artist", artist)
|
||||||
|
set_content(el, s.lyrics)
|
||||||
|
return subsonic_return(xdoc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return not_found()
|
||||||
|
end
|
||||||
|
|
||||||
|
unescape(s::AbstractString) = replace(s, '+' => ' ')
|
||||||
|
|
||||||
import Random
|
import Random
|
||||||
function getRandomSongs(; size = 10,
|
function getRandomSongs(; size = 10,
|
||||||
genre::Union{Missing,String} = missing,
|
genre::Union{Missing,String} = missing,
|
||||||
|
@ -256,31 +327,34 @@ function getRandomSongs(; size = 10,
|
||||||
toYear::Union{Missing,Int} = missing,
|
toYear::Union{Missing,Int} = missing,
|
||||||
musicFolderId::Union{Missing,String} = missing)
|
musicFolderId::Union{Missing,String} = missing)
|
||||||
songs = Beets.songs();
|
songs = Beets.songs();
|
||||||
# Randomize
|
|
||||||
songs = songs[Random.randperm(length(songs))];
|
|
||||||
# Filter
|
# Filter
|
||||||
!ismissing(genre) && filter!(x -> strip(x.genre) == strip(genre), songs)
|
genre = ismissing(genre) ? missing : strip(unescape(genre))
|
||||||
|
!ismissing(genre) && filter!(x -> strip(x.genre) == genre, songs)
|
||||||
!ismissing(fromYear) && filter!(x -> x.year > fromYear, songs)
|
!ismissing(fromYear) && filter!(x -> x.year > fromYear, songs)
|
||||||
!ismissing(toYear) && filter!(x -> x.year < toYear, songs)
|
!ismissing(toYear) && filter!(x -> x.year < toYear, songs)
|
||||||
|
# Randomize
|
||||||
|
songs = songs[Random.randperm(length(songs))];
|
||||||
# Take size
|
# Take size
|
||||||
songs = length(songs) > size ? songs[1:size] : songs;
|
songs = length(songs) > size ? songs[1:size] : songs;
|
||||||
# Create output
|
# Create output
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
list = new_child(xroot, "randomSongs")
|
list = new_child(xroot, "randomSongs")
|
||||||
albums = Beets.albums;
|
|
||||||
for song in songs
|
for song in songs
|
||||||
album = filter(x -> song in x.songs, albums) |> first
|
x = push!(list, song)
|
||||||
push!(list, (song, album))
|
alb = Beets.album(song)
|
||||||
|
set_attribute(x, "artist", alb.artist.name)
|
||||||
|
set_attribute(x, "coverArt", alb.uuid)
|
||||||
|
set_attribute(x, "album", alb.title)
|
||||||
end
|
end
|
||||||
return subsonic_return(xdoc)
|
return subsonic_return(xdoc)
|
||||||
end
|
end
|
||||||
|
|
||||||
function getRandomSongs(req)
|
function getRandomSongs(req)
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
# @show query
|
||||||
# name Required Default Notes
|
# name Required Default Notes
|
||||||
# size No 10 The maximum number of songs to return. Max 500.
|
# size No 10 The maximum number of songs to return. Max 500.
|
||||||
size = parse(Int, get(query, "size", "10"))
|
size = min(500,parse(Int, get(query, "size", "10")))
|
||||||
size = size > 500 ? 500 : size
|
|
||||||
# genre No Only returns songs belonging to this genre.
|
# genre No Only returns songs belonging to this genre.
|
||||||
genre = get(query, "genre", missing)
|
genre = get(query, "genre", missing)
|
||||||
# fromYear No Only return songs published after or in this year.
|
# fromYear No Only return songs published after or in this year.
|
||||||
|
@ -294,28 +368,234 @@ function getRandomSongs(req)
|
||||||
fromYear = fromY, toYear = toY)
|
fromYear = fromY, toYear = toY)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function getStarred(req; n = "")
|
||||||
|
global user_stars
|
||||||
|
starred = get(user_stars, req[:login][:name], Star[])
|
||||||
|
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
results = new_child(xroot, "starred$n")
|
||||||
|
for el in starred
|
||||||
|
x = push!(results, el.item)
|
||||||
|
set_attribute(x, "starred", el.starred)
|
||||||
|
if isa(el.item, Beets.Song)
|
||||||
|
alb = Beets.album(el.item)
|
||||||
|
set_attribute(x, "artist", alb.artist.name)
|
||||||
|
set_attribute(x, "coverArt", alb.uuid)
|
||||||
|
set_attribute(x, "album", alb.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return subsonic_return(xdoc)
|
||||||
|
end
|
||||||
|
|
||||||
|
getStarred2(req) = getStarred(req, n = "2")
|
||||||
|
|
||||||
|
function getids(query)
|
||||||
|
res = Dict{Symbol,Union{Nothing,AbstractString}}(
|
||||||
|
:id => "", :albumId => "", :artistId => "")
|
||||||
|
res[:id] = get(query, "id", nothing)
|
||||||
|
res[:id] != nothing && return res
|
||||||
|
res[:albumId] = get(query, "albumId", nothing)
|
||||||
|
res[:albumId] != nothing && return res
|
||||||
|
res[:artistId] = get(query, "artistId", nothing)
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function getanybyid(ids)
|
||||||
|
res = nothing
|
||||||
|
if ids[:id] !== nothing
|
||||||
|
res = Beets.songbyid(ids[:id])
|
||||||
|
res !== nothing && return res
|
||||||
|
end
|
||||||
|
if ids[:albumId] !== nothing
|
||||||
|
albums = Beets.albums
|
||||||
|
m = findfirst(x -> (x.uuid == ids[:albumId]), albums)
|
||||||
|
if m !== nothing
|
||||||
|
res = albums[m]
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ids[:artistId] !== nothing
|
||||||
|
artists = Beets.artists()
|
||||||
|
m = findfirst(x -> (x.uuid == ids[:artistId]), artists)
|
||||||
|
if m !== nothing
|
||||||
|
res = artists[m]
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
function star(req)
|
||||||
|
global user_stars
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
ids = getids(query)
|
||||||
|
any(values(ids) .!= nothing) ||
|
||||||
|
return missing_parameter("either id, artistId or albumId are required")
|
||||||
|
|
||||||
|
item = getanybyid(ids)
|
||||||
|
item == false && return not_found()
|
||||||
|
starred = get(user_stars, req[:login][:name], Star[])
|
||||||
|
m = findfirst(x -> x.item == item, starred)
|
||||||
|
# Alredy added
|
||||||
|
m != nothing && return @subsonic(nothing)
|
||||||
|
push!(starred, Star(item, Dates.now()))
|
||||||
|
user_stars[req[:login][:name]] = starred
|
||||||
|
savestarred()
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
function unstar(req)
|
||||||
|
global user_stars
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
ids = getids(query)
|
||||||
|
any(values(ids) .!= nothing) ||
|
||||||
|
return missing_parameter("either id, artistId or albumId are required")
|
||||||
|
|
||||||
|
item = getanybyid(ids)
|
||||||
|
item == false && not_found()
|
||||||
|
|
||||||
|
starred = get(user_stars, req[:login][:name], Star[])
|
||||||
|
# @show length(starred[1].item.uuid)
|
||||||
|
m = findfirst(s -> s.item.uuid == item.uuid, starred)
|
||||||
|
m === nothing && return not_found()
|
||||||
|
|
||||||
|
deleteat!(starred, m)
|
||||||
|
user_stars[req[:login][:name]] = starred
|
||||||
|
savestarred()
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
function setRating(req)
|
||||||
|
global user_ratings
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
id = get(query, "id", nothing)
|
||||||
|
id === nothing && return not_found("id")
|
||||||
|
rating = get(query, "rating", "")
|
||||||
|
isempty(rating) && return missing_parameter("rating")
|
||||||
|
rating = parse(Int, rating)
|
||||||
|
item = Beets.songbyid(id)
|
||||||
|
if item === nothing
|
||||||
|
item = Beets.album(id)
|
||||||
|
end
|
||||||
|
if item === nothing
|
||||||
|
item = Beets.artistbyid(id)
|
||||||
|
end
|
||||||
|
item === nothing && return not_found("id")
|
||||||
|
ratings = get(user_ratings, req[:login][:name], Rating[])
|
||||||
|
if rating == 0
|
||||||
|
@info "Removing"
|
||||||
|
# remove
|
||||||
|
m = findfirst(x -> x.item.uuid == item.uuid, ratings)
|
||||||
|
m === nothing && return not_found("id")
|
||||||
|
deleteat!(ratings, m)
|
||||||
|
else
|
||||||
|
rating = max(min(rating, 5), 1)
|
||||||
|
# update or add
|
||||||
|
m = findfirst(x -> x.item == item, ratings)
|
||||||
|
if m === nothing
|
||||||
|
push!(ratings, Rating(item, rating))
|
||||||
|
else
|
||||||
|
ratings[m].rating = rating
|
||||||
|
end
|
||||||
|
end
|
||||||
|
user_ratings[req[:login][:name]] = ratings
|
||||||
|
saveratings()
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
function extend(d::Dict, el::Pair)
|
||||||
|
if el[1] in keys(d)
|
||||||
|
push!(d[el[1]], el[2])
|
||||||
|
else
|
||||||
|
d[el[1]] = [el[2]]
|
||||||
|
end
|
||||||
|
d
|
||||||
|
end
|
||||||
|
|
||||||
|
function savePlayQueue(req)
|
||||||
|
@info "savePlayQueue"
|
||||||
|
global user_playqueue
|
||||||
|
if ! (:data in keys(req))
|
||||||
|
return @subsonic(nothing)
|
||||||
|
end
|
||||||
|
data = String(req[:data])
|
||||||
|
req[:data] = Dict{String,Any}()
|
||||||
|
x = map(x -> let d = split(x, '='); (d[1] => d[2]) end, split(data, '&'))
|
||||||
|
map(x -> extend(req[:data], x), x)
|
||||||
|
# @show req[:data]
|
||||||
|
user_playqueue[req[:login][:name]] = string.(req[:data]["id"])
|
||||||
|
if "current" in keys(req[:data])
|
||||||
|
# TODO: increase play count?
|
||||||
|
end
|
||||||
|
return @subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
function getPlayQueue(req)
|
||||||
|
@info "getPlayQueue"
|
||||||
|
global user_playqueue
|
||||||
|
(req[:login][:name] in keys(user_playqueue)) || return @subsonic(nothing)
|
||||||
|
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
queue = new_child(xroot, "playQueue")
|
||||||
|
# current="133" position="45000" username="admin" changed="2015-02-18T15:22:22.825Z" changedBy="android"
|
||||||
|
set_attributes(queue, [("current", "0"),
|
||||||
|
("position", "0"),
|
||||||
|
("username", req[:login][:name]),
|
||||||
|
("changed", "0"),
|
||||||
|
("changedBy", "FIXME")
|
||||||
|
])
|
||||||
|
for id in user_playqueue[req[:login][:name]]
|
||||||
|
# push item
|
||||||
|
end
|
||||||
|
|
||||||
|
return subsonic_return(xdoc)
|
||||||
|
end
|
||||||
|
|
||||||
|
function debug(req)
|
||||||
|
if :data in keys(req)
|
||||||
|
req[:data] = String(req[:data])
|
||||||
|
end
|
||||||
|
# @show req
|
||||||
|
return JlSonic.@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
function match(q::Regex, album::Beets.Album)
|
function match(q::Regex, album::Beets.Album)
|
||||||
(match(q, album.title) !== nothing) || (match(q, album.artist.name) !== nothing) ||
|
(match(q, album.title) !== nothing) || (match(q, album.artist.name) !== nothing) ||
|
||||||
any(map(s -> match(q, s.title) !== nothing, album.songs))
|
any(map(s -> match(q, s.title) !== nothing, album.songs))
|
||||||
end
|
end
|
||||||
|
|
||||||
function makequery(q::String)
|
function makequery(q::AbstractString)
|
||||||
nq = replace(lowercase(q), "*" => ".*")
|
# nq = replace(lowercase(q), "*" => ".*")
|
||||||
nq = string(".*", nq)
|
nq = lowercase(q)
|
||||||
|
nq = string(".*", nq, ".*")
|
||||||
return Regex(nq)
|
return Regex(nq)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Deprecated but used by dsub
|
||||||
|
function search(req; dlalbum = println, dlall = println)
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
artist = get(query, "artist", "")
|
||||||
|
album= get(query, "album", "")
|
||||||
|
title = get(query, "title", "")
|
||||||
|
any = get(query, "any", "")
|
||||||
|
count = get(query, "count", "20")
|
||||||
|
# Used by dsub, but not in the specifications
|
||||||
|
songCount = get(query, "songCount", "20")
|
||||||
|
offset = get(query, "offset", "0")
|
||||||
|
newerThan = get(query, "newerThan", "")
|
||||||
|
end
|
||||||
|
|
||||||
function search3(req; dlalbum = println, dlall = println)
|
function search3(req; dlalbum = println, dlall = println)
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
q = get(query, "query", "")
|
q = get(query, "query", "")
|
||||||
|
q = isempty(q) ? get(query, "any", "") : q
|
||||||
isempty(q) && return missing_parameter("query")
|
isempty(q) && return missing_parameter("query")
|
||||||
if req[:login][:user].upload && length(q) > 2 && q[end-1:end] == "!a"
|
length(q) < 3 && return missing_parameter("query should be at least 3 char long")
|
||||||
|
if length(q) > 2 && q[end-1:end] == "!a" && req[:login][:user].upload
|
||||||
@info "Downloading single album"
|
@info "Downloading single album"
|
||||||
dlalbum(string(strip(q[1:end-2])))
|
dlalbum(string(strip(q[1:end-2])))
|
||||||
return @subsonic(nothing)
|
return @subsonic(nothing)
|
||||||
elseif req[:login][:user].upload && length(q) > 2 && q[end-1:end] == "!d"
|
elseif length(q) > 2 && q[end-1:end] == "!d" && req[:login][:user].upload
|
||||||
@info "Downloading discography!"
|
@info "Downloading discography!"
|
||||||
dlall(string(strip(q[1:end-2])))
|
dlall(string(strip(q[1:end-2])))
|
||||||
return @subsonic(nothing)
|
return @subsonic(nothing)
|
||||||
|
@ -335,18 +615,15 @@ function search3(req; dlalbum = println, dlall = println)
|
||||||
# musicFolderId No (Since 1.12.0) Only return results from music folder with the given ID. See getMusicFolders.
|
# musicFolderId No (Since 1.12.0) Only return results from music folder with the given ID. See getMusicFolders.
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
results = new_child(xroot, "searchResult3")
|
results = new_child(xroot, "searchResult3")
|
||||||
k = makequery(string(q))
|
k = map(x -> makequery(x), split.(unescape(string(q)), ' '))
|
||||||
|
# @show k
|
||||||
matchingartists = Beets.artists()
|
matchingartists = Beets.artists()
|
||||||
filter!(a -> Base.match(k, lowercase(a.name)) !== nothing, matchingartists)
|
filter!(a -> all(Base.match.(k, lowercase(a.name)) .!== nothing), matchingartists)
|
||||||
albums = Beets.albums;
|
matchingalbums = deepcopy(Beets.albums)
|
||||||
matchingalbums = filter(a -> Base.match(k, lowercase(a.title)) !== nothing,
|
filter!(a -> all(Base.match.(k, lowercase(a.title)) .!== nothing), matchingalbums)
|
||||||
albums)
|
matchingsongs = Beets.songs()
|
||||||
matchingsongs = Tuple{Beets.Song,Beets.Album}[]
|
filter!(s -> all(Base.match.(k, lowercase(s.title)) .!== nothing), matchingsongs)
|
||||||
for album in albums, song in album.songs
|
matchingsongs = length(matchingsongs) > songCount ? matchingsongs[1:songCount] : matchingsongs
|
||||||
if Base.match(k, lowercase(song.title)) !== nothing
|
|
||||||
push!(matchingsongs, (song, album))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Artists
|
# Artists
|
||||||
push!.(Ref(results), matchingartists)
|
push!.(Ref(results), matchingartists)
|
||||||
# # Albums
|
# # Albums
|
||||||
|
@ -386,7 +663,6 @@ function createPlaylist(req)
|
||||||
name = name # cover = ???
|
name = name # cover = ???
|
||||||
)
|
)
|
||||||
push!(playlist, song)
|
push!(playlist, song)
|
||||||
@show "THERE"
|
|
||||||
else
|
else
|
||||||
return missing_parameter("either name or playlistId")
|
return missing_parameter("either name or playlistId")
|
||||||
end
|
end
|
||||||
|
@ -402,7 +678,7 @@ end
|
||||||
function getPlaylists(req)
|
function getPlaylists(req)
|
||||||
global user_playlists
|
global user_playlists
|
||||||
# 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()
|
||||||
playlistsXML = new_child(xroot, "playlists")
|
playlistsXML = new_child(xroot, "playlists")
|
||||||
for playlist in sort(filter(p -> canread(req[:login][:user], p),
|
for playlist in sort(filter(p -> canread(req[:login][:user], p),
|
||||||
user_playlists),
|
user_playlists),
|
||||||
|
@ -413,11 +689,14 @@ function getPlaylists(req)
|
||||||
end
|
end
|
||||||
|
|
||||||
import Base.get
|
import Base.get
|
||||||
function get(::Type{Playlist}, u::User, id::AbstractString)::Union{Nothing,Playlist}
|
get(::Type{Playlist}, u::User, id::AbstractString)::Union{Nothing,Playlist} =
|
||||||
|
get(Playlist, u, PlaylistUUID(id))
|
||||||
|
|
||||||
|
function get(::Type{Playlist}, u::User, id::PlaylistUUID)::Union{Nothing,Playlist}
|
||||||
global user_playlists
|
global user_playlists
|
||||||
playlists = filter(p -> canread(u, p), user_playlists)
|
playlists = filter(p -> canread(u, p), user_playlists)
|
||||||
m = findfirst(p -> p.uuid == id, playlists)
|
m = findfirst(p -> p.uuid == id, playlists)
|
||||||
m == nothing ? nothing : playlists[m]
|
m === nothing ? nothing : playlists[m]
|
||||||
end
|
end
|
||||||
|
|
||||||
"Returns a listing of files in a saved playlist."
|
"Returns a listing of files in a saved playlist."
|
||||||
|
@ -427,7 +706,7 @@ function getPlaylist(req)
|
||||||
id = get(query, "id", "")
|
id = get(query, "id", "")
|
||||||
isempty(id) && return missing_parameter("id")
|
isempty(id) && return missing_parameter("id")
|
||||||
playlist = get(Playlist, req[:login][:user], id)
|
playlist = get(Playlist, req[:login][:user], id)
|
||||||
playlist == nothing && return not_found("id")
|
playlist === nothing && return not_found("id")
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
append!(xroot, playlist)
|
append!(xroot, playlist)
|
||||||
return subsonic_return(xroot)
|
return subsonic_return(xroot)
|
||||||
|
@ -440,10 +719,10 @@ function updatePlaylist(req)
|
||||||
playlistId = get(query, "playlistId", "")
|
playlistId = get(query, "playlistId", "")
|
||||||
isempty(playlistId) && return missing_parameter("playlistId")
|
isempty(playlistId) && return missing_parameter("playlistId")
|
||||||
playlist = get(Playlist, req[:login][:user], playlistId)
|
playlist = get(Playlist, req[:login][:user], playlistId)
|
||||||
playlist == nothing && return not_found("playlistId")
|
playlist === nothing && return not_found("playlistId")
|
||||||
|
|
||||||
# Check ownership (if not allowed, should not even reach this (canread is false))
|
# Check ownership (if not allowed, should not even reach this (canread is false))
|
||||||
canedit(req[:login][:user], playlist) || return unuthorized()
|
canedit(req[:login][:user], playlist) || return unauthorized()
|
||||||
# Want to make public. Is allowed?
|
# Want to make public. Is allowed?
|
||||||
wantpublic = try
|
wantpublic = try
|
||||||
parse(Bool,get(query, "public", string(playlist.public)))
|
parse(Bool,get(query, "public", string(playlist.public)))
|
||||||
|
@ -451,14 +730,14 @@ function updatePlaylist(req)
|
||||||
isa(e, ArgumentError) ? false : @error e
|
isa(e, ArgumentError) ? false : @error e
|
||||||
end
|
end
|
||||||
if wantpublic
|
if wantpublic
|
||||||
canmakepublic(req[:login][:user]) || return unuthorized()
|
canmakepublic(req[:login][:user]) || return unauthorized()
|
||||||
playlist.public = true
|
playlist.public = true
|
||||||
else
|
else
|
||||||
playlist.public = false
|
playlist.public = false
|
||||||
end
|
end
|
||||||
|
|
||||||
playlist.name = get(query, "name", playlist.name)
|
playlist.name = get(query, "name", playlist.name)
|
||||||
playlist.comment = get(query, "comment", playlist.comment)
|
playlist.comment = unescape(get(query, "comment", playlist.comment))
|
||||||
songIdAdd = get(query, "songIdToAdd", "")
|
songIdAdd = get(query, "songIdToAdd", "")
|
||||||
# WTF by the index!?
|
# WTF by the index!?
|
||||||
IndexToRemove = get(query, "songIndexToRemove", "")
|
IndexToRemove = get(query, "songIndexToRemove", "")
|
||||||
|
@ -486,10 +765,10 @@ function deletePlaylist(req)
|
||||||
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, user_playlists)
|
m = findfirst(p -> p.uuid == PlaylistUUID(id), user_playlists)
|
||||||
m === nothing && return not_found("id")
|
m === nothing && return not_found("id")
|
||||||
if !canedit(req[:login][:user], user_playlists[m])
|
if !canedit(req[:login][:user], user_playlists[m])
|
||||||
return unuthorized()
|
return unauthorized()
|
||||||
end
|
end
|
||||||
|
|
||||||
deleteat!(user_playlists, m)
|
deleteat!(user_playlists, m)
|
||||||
|
@ -498,9 +777,28 @@ function deletePlaylist(req)
|
||||||
@subsonic(nothing)
|
@subsonic(nothing)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function scrobble(req)
|
||||||
|
# @show req
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
function getUser(req)
|
function getUser(req)
|
||||||
|
global users
|
||||||
|
m = findfirst(x -> x.name == req[:login][:name], users)
|
||||||
|
m === nothing && return not_found()
|
||||||
(xdoc, xroot) = subsonic()
|
(xdoc, xroot) = subsonic()
|
||||||
push!(xroot, User(req[:login][:name]))
|
push!(xroot, users[m])
|
||||||
|
return subsonic_return(xroot)
|
||||||
|
end
|
||||||
|
|
||||||
|
function getUsers(req)
|
||||||
|
req[:login][:user].admin || return unauthorized()
|
||||||
|
global users
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
usersXML = new_child(xroot, "users")
|
||||||
|
for user in users
|
||||||
|
push!(usersXML, user)
|
||||||
|
end
|
||||||
return subsonic_return(xroot)
|
return subsonic_return(xroot)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -509,23 +807,26 @@ const song_cache = Dict{Tuple{String,Int,String}, Vector{UInt8}}()
|
||||||
|
|
||||||
# Media retriveal
|
# Media retriveal
|
||||||
"Returns a cover art image."
|
"Returns a cover art image."
|
||||||
function getCoverArt(req::Dict)
|
function getCoverArt(req::Dict, config)
|
||||||
global cover_cache
|
global cover_cache
|
||||||
|
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
|
||||||
|
px = get(query, "size", "")
|
||||||
|
px = isempty(px) ? config[:cover][:size] : parse(Int, px)
|
||||||
|
|
||||||
id = get(query, "id", "")
|
id = get(query, "id", "")
|
||||||
isempty(id) && return missing_parameter("id")
|
isempty(id) && return missing_parameter("id")
|
||||||
n = findfirst(a -> a.uuid == id, Beets.albums)
|
n = findfirst(a -> a.uuid == id, Beets.albums)
|
||||||
|
|
||||||
(n === nothing || isempty(Beets.albums[n].cover)) && return not_found("id")
|
n === nothing && return not_found("id")
|
||||||
|
isempty(Beets.albums[n].cover) && return not_found("album has no cover art")
|
||||||
|
|
||||||
if Beets.albums[n].cover in keys(cover_cache)
|
if Beets.albums[n].cover in keys(cover_cache)
|
||||||
data = cover_cache[Beets.albums[n].cover]
|
data = cover_cache[Beets.albums[n].cover]
|
||||||
else
|
else
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
p = run(pipeline(`convert $(Beets.albums[n].cover) -resize 200x200 png:-`,
|
p = run(pipeline(`convert $(Beets.albums[n].cover) -resize $(px)x$(px) png:-`
|
||||||
stderr=devnull, stdout=io), wait = true)
|
, stderr=devnull, stdout=io), wait = true)
|
||||||
data = take!(io)
|
data = take!(io)
|
||||||
cover_cache[Beets.albums[n].cover] = data
|
cover_cache[Beets.albums[n].cover] = data
|
||||||
end
|
end
|
||||||
|
@ -541,14 +842,16 @@ function getCoverArt(req::Dict)
|
||||||
:file => join([split(basename(Beets.albums[n].cover), '.')[1:end-1], ".png"],""))
|
:file => join([split(basename(Beets.albums[n].cover), '.')[1:end-1], ".png"],""))
|
||||||
end
|
end
|
||||||
|
|
||||||
function giveconverted(file, bitrate, format)
|
function giveconverted(file, bitrate, format; stream = false)
|
||||||
global song_cache
|
global song_cache
|
||||||
k = (file, bitrate, format)
|
k = (file, bitrate, format)
|
||||||
if k in keys(song_cache)
|
if k in keys(song_cache)
|
||||||
@info "Using cached"
|
@info "Using cached"
|
||||||
data = song_cache[k]
|
data = song_cache[k]
|
||||||
else
|
else
|
||||||
@info "Adding song to cache"
|
let cache_size = Base.summarysize(song_cache)
|
||||||
|
@info "Adding song ($file) to cache, cache size is $(datasize(cache_size))"
|
||||||
|
end
|
||||||
iodata = convert(file, bitrate = bitrate, format = format)
|
iodata = convert(file, bitrate = bitrate, format = format)
|
||||||
data = take!(iodata)
|
data = take!(iodata)
|
||||||
try
|
try
|
||||||
|
@ -560,8 +863,12 @@ function giveconverted(file, bitrate, format)
|
||||||
|
|
||||||
headers = Dict{String,String}()
|
headers = Dict{String,String}()
|
||||||
suffix = format
|
suffix = format
|
||||||
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] :
|
if stream
|
||||||
|
mime = "application/octet-stream .$(format)"
|
||||||
|
else
|
||||||
|
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] :
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
|
end
|
||||||
headers["Content-Type"] = mime
|
headers["Content-Type"] = mime
|
||||||
headers["Content-Length"] = string(length(data))
|
headers["Content-Length"] = string(length(data))
|
||||||
# headers["Transfer-Encoding"] = "chunked"
|
# headers["Transfer-Encoding"] = "chunked"
|
||||||
|
@ -574,7 +881,8 @@ end
|
||||||
function convert(infile; bitrate = 64, format = "oga")
|
function convert(infile; bitrate = 64, format = "oga")
|
||||||
global ffmpeg_threads
|
global ffmpeg_threads
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
p = run(pipeline(`ffmpeg -i $infile -y -c:a libvorbis -b:a $(bitrate)k -threads $(ffmpeg_threads) -f $format pipe:1`,
|
# ar is required to convert 192kHz audio files to ogg
|
||||||
|
p = run(pipeline(`ffmpeg -i $infile -y -c:a libvorbis -b:a $(bitrate)k -threads $(ffmpeg_threads) -ar 44100 -f $format pipe:1`,
|
||||||
stderr=devnull, stdout=io), wait = true)
|
stderr=devnull, stdout=io), wait = true)
|
||||||
io
|
io
|
||||||
end
|
end
|
||||||
|
@ -582,10 +890,8 @@ end
|
||||||
canstream(u::User) = u.stream
|
canstream(u::User) = u.stream
|
||||||
"Streams a given media file."
|
"Streams a given media file."
|
||||||
function stream(req::Dict)
|
function stream(req::Dict)
|
||||||
@show req
|
|
||||||
|
|
||||||
query = HTTP.URIs.queryparams(req[:query])
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
canstream(req[:login][:user]) || return unuthorized()
|
canstream(req[:login][:user]) || return unauthorized()
|
||||||
|
|
||||||
id = get(query, "id", "")
|
id = get(query, "id", "")
|
||||||
isempty(id) && return missing_parameter("id")
|
isempty(id) && return missing_parameter("id")
|
||||||
|
@ -610,7 +916,7 @@ end
|
||||||
|
|
||||||
function sendfile(path; suffix = nothing)
|
function sendfile(path; suffix = nothing)
|
||||||
isfile(path) || return Dict{String,String}(:body => "Not Found")
|
isfile(path) || return Dict{String,String}(:body => "Not Found")
|
||||||
suffix = suffix == nothing ? lowercase(split(path, '.')[end]) : suffix
|
suffix = suffix === nothing ? lowercase(split(path, '.')[end]) : suffix
|
||||||
headers = Dict{String,String}()
|
headers = Dict{String,String}()
|
||||||
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] :
|
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] :
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
|
@ -621,13 +927,142 @@ function sendfile(path; suffix = nothing)
|
||||||
:headers => headers)
|
:headers => headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
canread(u::User, p::Playlist) = p.public ||
|
canread(u::User, p::Playlist) = p.public || (u.admin || p.owner == u.name) || u in p.allowed
|
||||||
(u.admin || p.owner == u.name) ||
|
canedit(u::User, p::Playlist) = (p.owner == u.name) || u.admin
|
||||||
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
|
|
||||||
canmakepublic(u::User) = u.playlist
|
canmakepublic(u::User) = u.playlist
|
||||||
|
|
||||||
|
function getShares(req)
|
||||||
|
global shared
|
||||||
|
share = filter(s -> s.username == req[:login][:name], shared)
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
s = new_child(xroot, "shares")
|
||||||
|
# @show share
|
||||||
|
try
|
||||||
|
push!.(Ref(s), share)
|
||||||
|
catch x
|
||||||
|
@show x
|
||||||
|
end
|
||||||
|
@show xroot
|
||||||
|
return subsonic_return(xroot)
|
||||||
|
end
|
||||||
|
|
||||||
|
# createShare
|
||||||
|
# id Yes ID of a song, album or video to share. Use one id parameter for each entry to share.
|
||||||
|
# description No A user-defined description that will be displayed to people visiting the shared media.
|
||||||
|
# expires No The time at which the share expires. Given as milliseconds since 1970.
|
||||||
|
# http://www.subsonic.org/pages/inc/api/examples/shares_example_1.xml
|
||||||
|
|
||||||
|
function createShare(req)
|
||||||
|
global shared
|
||||||
|
# check permissions
|
||||||
|
req[:login][:user].share || return unauthorized()
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
id = get(query, "id", "")
|
||||||
|
isempty(id) && return missing_parameter("id")
|
||||||
|
description = unescape(get(query, "description", "You have been shared this file"))
|
||||||
|
expires = get(query, "expires", "")
|
||||||
|
e = if isempty(expires)
|
||||||
|
Dates.now() + Dates.Day(7)
|
||||||
|
else
|
||||||
|
try
|
||||||
|
Dates.unix2datetime(parse(Int, expires) / 1000)
|
||||||
|
catch x
|
||||||
|
@show isa(x, ArgumentError)
|
||||||
|
@show x
|
||||||
|
return missing_parameter("id")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
item = getanybyid(Dict(:id => id, :artistId => id, :albumId => id))
|
||||||
|
item === nothing && not_found()
|
||||||
|
newshare = Share(item, req[:login][:name], description,
|
||||||
|
expires = e)
|
||||||
|
push!(shared, newshare)
|
||||||
|
saveshared()
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
s = new_child(xroot, "shares")
|
||||||
|
push!(s, newshare)
|
||||||
|
return subsonic_return(xroot)
|
||||||
|
end
|
||||||
|
|
||||||
|
expired(s::Share) = Dates.now() > s.expires
|
||||||
|
|
||||||
|
function updateShare(req)
|
||||||
|
global shared
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
id = get(query, "id", "")
|
||||||
|
isempty(id) && return missing_parameter("id")
|
||||||
|
id = try
|
||||||
|
parse(UInt32, id)
|
||||||
|
catch e
|
||||||
|
@show isa(e, ArgumentError)
|
||||||
|
@show e
|
||||||
|
return missing_parameter("id")
|
||||||
|
end
|
||||||
|
m = findfirst(x -> x.id == id, shared)
|
||||||
|
m === nothing && return not_found("id")
|
||||||
|
shared[m].username == req[:login][:name] || return unauthorized()
|
||||||
|
shared[m].description = get(query, "description", shared[m].description)
|
||||||
|
expires = get(query, "expires", "")
|
||||||
|
e = if isempty(expires)
|
||||||
|
Dates.now() + Dates.Day(7)
|
||||||
|
else
|
||||||
|
try
|
||||||
|
Dates.unix2datetime(parse(Int, expires) / 1000)
|
||||||
|
catch x
|
||||||
|
@show isa(x, ArgumentError)
|
||||||
|
@show x
|
||||||
|
return missing_parameter("id")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
shared[m].expires = e
|
||||||
|
if expired(shared[m])
|
||||||
|
deleteat!(shared, m)
|
||||||
|
saveshared()
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
saveshared()
|
||||||
|
(xdoc, xroot) = subsonic()
|
||||||
|
s = new_child(xroot, "shares")
|
||||||
|
push!(s, shared[m])
|
||||||
|
return subsonic_return(xroot)
|
||||||
|
end
|
||||||
|
|
||||||
|
function deleteShare(req)
|
||||||
|
global shared
|
||||||
|
query = HTTP.URIs.queryparams(req[:query])
|
||||||
|
id = get(query, "id", "")
|
||||||
|
isempty(id) && return missing_parameter("id")
|
||||||
|
id = parse(UInt32, id)
|
||||||
|
m = findfirst(x -> x.id == id, shared)
|
||||||
|
m === nothing && return not_found("id")
|
||||||
|
shared[m].username == req[:login][:name] || return unauthorized()
|
||||||
|
# Delete it
|
||||||
|
deleteat!(shared, m)
|
||||||
|
saveshared()
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateUser(req)
|
||||||
|
@show req
|
||||||
|
|
||||||
|
# username Yes The name of the user.
|
||||||
|
# password No The password of the user, either in clear text of hex-encoded (see above).
|
||||||
|
# email No The email address of the user.
|
||||||
|
# ldapAuthenticated No Whether the user is authenicated in LDAP.
|
||||||
|
# adminRole No Whether the user is administrator.
|
||||||
|
# settingsRole No Whether the user is allowed to change personal settings and password.
|
||||||
|
# streamRole No Whether the user is allowed to play files.
|
||||||
|
# jukeboxRole No Whether the user is allowed to play files in jukebox mode.
|
||||||
|
# downloadRole No Whether the user is allowed to download files.
|
||||||
|
# uploadRole No Whether the user is allowed to upload files.
|
||||||
|
# coverArtRole No Whether the user is allowed to change cover art and tags.
|
||||||
|
# commentRole No Whether the user is allowed to create and edit comments and ratings.
|
||||||
|
# podcastRole No Whether the user is allowed to administrate Podcasts.
|
||||||
|
# shareRole No Whether the user is allowed to share files with anyone.
|
||||||
|
# videoConversionRole No false (Since 1.15.0) Whether the user is allowed to start video conversions.
|
||||||
|
# musicFolderId No (Since 1.12.0) IDs of the music folders the user is allowed access to. Include the parameter once for each folder.
|
||||||
|
# maxBitRate No (Since 1.13.0) The maximum bit rate (in Kbps) for the user. Audio streams of higher bit rates are automatically downsampled to this bit rate. Legal values: 0 (no limit), 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320.
|
||||||
|
|
||||||
|
|
||||||
|
@subsonic(nothing)
|
||||||
|
end
|
||||||
|
|
|
@ -10,43 +10,75 @@ Mux.mimetypes["m4a"] = "audio/x-m4a"
|
||||||
function push!(root::XMLElement, p::Playlist)
|
function push!(root::XMLElement, p::Playlist)
|
||||||
playlistXML = new_child(root, "playlist")
|
playlistXML = new_child(root, "playlist")
|
||||||
set_attributes(playlistXML, [
|
set_attributes(playlistXML, [
|
||||||
("id", p.uuid),
|
("id", convert(String, p.uuid)),
|
||||||
("name", p.name),
|
("name", p.name),
|
||||||
("comment", p.comment),
|
("comment", p.comment),
|
||||||
("owner", p.owner),
|
("owner", p.owner),
|
||||||
("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(+,
|
||||||
|
Beets.songbyid.(p.songs),
|
||||||
|
init = 0.0) |> floor |> Int |> string),
|
||||||
("created", ms2string(p.created)),
|
("created", ms2string(p.created)),
|
||||||
("coverArt", p.cover),
|
("coverArt", p.cover),
|
||||||
])
|
])
|
||||||
playlistXML
|
playlistXML
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# FIXME: save uuids in playlists and when importing them
|
||||||
|
# look for the correct song. metadata can change
|
||||||
function append!(root::XMLElement, p::Playlist)
|
function append!(root::XMLElement, p::Playlist)
|
||||||
playlistXML = push!(root, p)
|
playlistXML = push!(root, p)
|
||||||
# Allowed users
|
# Allowed users
|
||||||
for al in p.allowed
|
for al in p.allowed
|
||||||
set_content(new_child(playlistXML, "allowedUser"), al)
|
set_content(new_child(playlistXML, "allowedUser"), al)
|
||||||
end
|
end
|
||||||
for song in p.songs
|
@info "OK"
|
||||||
|
for song in Beets.songbyid.(p.songs)
|
||||||
|
@show song
|
||||||
entry = new_child(playlistXML, "entry")
|
entry = new_child(playlistXML, "entry")
|
||||||
set_attributes(entry, props(song))
|
set_attributes(entry, props(song))
|
||||||
|
album = Beets.album(song)
|
||||||
|
set_attribute(entry, "coverArt", album.uuid)
|
||||||
try
|
try
|
||||||
artist = Beets.artist(song)
|
artist = Beets.artist(song)
|
||||||
n = artist == nothing ? artist.name : ""
|
n = artist != nothing ? artist.name : ""
|
||||||
set_attribute(entry, "artist", "")
|
set_attribute(entry, "artist", n)
|
||||||
catch e
|
catch e
|
||||||
@warn e
|
@warn e
|
||||||
|
@show song.uuid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
playlistXML
|
playlistXML
|
||||||
end
|
end
|
||||||
|
|
||||||
push!(p::Playlist, s::Song) = push!(p.songs, s)
|
# FIXME!!
|
||||||
|
function push!(root::XMLElement, s::Share)
|
||||||
|
shareXML = new_child(root, "share")
|
||||||
|
set_attributes(shareXML, [
|
||||||
|
("id", string(s.id)),
|
||||||
|
# FIXME: Don't hardcode
|
||||||
|
("url", string("https://music.", domain, "/share/", s.uuid)),
|
||||||
|
("description", s.description),
|
||||||
|
("username", s.username),
|
||||||
|
("created", ms2string(s.created)),
|
||||||
|
("lastVisited", ismissing(s.lastvisit) ? "" : ms2string(s.lastvisit)),
|
||||||
|
("expires", ms2string(s.expires)),
|
||||||
|
("visitCount", string(s.count))
|
||||||
|
])
|
||||||
|
if isa(s.item, Album)
|
||||||
|
push!.(Ref(shareXML), s.item.songs; element = "entry")
|
||||||
|
else
|
||||||
|
push!(shareXML, s.item; element = "entry")
|
||||||
|
end
|
||||||
|
shareXML
|
||||||
|
end
|
||||||
|
|
||||||
function push!(root::XMLElement, album::Beets.Album)
|
push!(p::Playlist, s::Song) = push!(p.songs, SongUUID(s.uuid))
|
||||||
albumXML = new_child(root, "album")
|
push!(p::Playlist, s::SongUUID) = push!(p.songs, s)
|
||||||
|
|
||||||
|
function push!(root::XMLElement, album::Beets.Album; element = "album")
|
||||||
|
albumXML = new_child(root, element)
|
||||||
set_attributes(albumXML, [
|
set_attributes(albumXML, [
|
||||||
("id", album.uuid),
|
("id", album.uuid),
|
||||||
("name", album.title),
|
("name", album.title),
|
||||||
|
@ -91,8 +123,8 @@ function push!(root::XMLElement, artist::Beets.Artist)
|
||||||
artistXML
|
artistXML
|
||||||
end
|
end
|
||||||
|
|
||||||
function push!(root::XMLElement, song::Beets.Song)
|
function push!(root::XMLElement, song::Beets.Song; element = "song")
|
||||||
songXML = new_child(root, "song")
|
songXML = new_child(root, element)
|
||||||
suffix = lowercase(song.format)
|
suffix = lowercase(song.format)
|
||||||
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
|
mime = suffix in keys(Mux.mimetypes) ? Mux.mimetypes[suffix] : suffix
|
||||||
set_attributes(songXML, [
|
set_attributes(songXML, [
|
||||||
|
@ -150,7 +182,7 @@ function props(song::Song)
|
||||||
("album", song.title),
|
("album", song.title),
|
||||||
# ("artist", song.album.artist.name),
|
# ("artist", song.album.artist.name),
|
||||||
("isDir", "false"),
|
("isDir", "false"),
|
||||||
("coverArt", song.uuid),
|
# ("coverArt", song.uuid),
|
||||||
("created", ms2string(song.added)),
|
("created", ms2string(song.added)),
|
||||||
("duration", string(floor(song.length) |> Int)),
|
("duration", string(floor(song.length) |> Int)),
|
||||||
("bitrate", string(song.bitrate)),
|
("bitrate", string(song.bitrate)),
|
||||||
|
@ -165,6 +197,18 @@ function props(song::Song)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function push!(root::XMLElement, songs::Vector{Beets.Song})
|
||||||
|
for song in songs
|
||||||
|
album = Beets.album(song)
|
||||||
|
songXML = new_child(root, "song")
|
||||||
|
set_attributes(songXML, props(song))
|
||||||
|
set_attribute(songXML, "artistId", album.artist.uuid)
|
||||||
|
set_attribute(songXML, "albumId", album.uuid)
|
||||||
|
set_attribute(songXML, "artist", album.artist.name)
|
||||||
|
set_attribute(songXML, "album", album.title)
|
||||||
|
end
|
||||||
|
root
|
||||||
|
end
|
||||||
|
|
||||||
function push!(root::XMLElement, songs::Vector{Tuple{Beets.Song,Beets.Album}})
|
function push!(root::XMLElement, songs::Vector{Tuple{Beets.Song,Beets.Album}})
|
||||||
for (song, album) in songs
|
for (song, album) in songs
|
||||||
|
|
|
@ -16,6 +16,14 @@ function loadplaylists(; file = playlistfile(Beets.confdir()))
|
||||||
for p in ps
|
for p in ps
|
||||||
try
|
try
|
||||||
pl = JSON2.read(p, Playlist)
|
pl = JSON2.read(p, Playlist)
|
||||||
|
# TODO: Control verbosity
|
||||||
|
@info "Importing playlist ($(pl.owner)) $(convert(String, pl.uuid)), with $(length(pl.songs)) songs in it"
|
||||||
|
# Check if song uuid exits, else skip it (and emit a warning)
|
||||||
|
filteredsongs = filter(x -> Beets.songbyid(x) !== nothing, pl.songs)
|
||||||
|
if length(filteredsongs) != length(pl.songs)
|
||||||
|
pl.songs = filteredsongs
|
||||||
|
@warn "Failed to import some playlist's song"
|
||||||
|
end
|
||||||
push!(user_playlists, pl)
|
push!(user_playlists, pl)
|
||||||
catch e
|
catch e
|
||||||
@warn "Failed to read with error $e"
|
@warn "Failed to read with error $e"
|
||||||
|
@ -24,14 +32,15 @@ function loadplaylists(; file = playlistfile(Beets.confdir()))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function saveusers(file = expanduser("~/.config/beets/users.jsonl"))
|
beetfile(f) = expanduser("~/.config/beets/$f")
|
||||||
|
function saveusers(file = beetfile("users.jsonl"))
|
||||||
global users
|
global users
|
||||||
open(file, "w") do f
|
open(file, "w") do f
|
||||||
write(f, join(JSON2.write.(users), "\n"))
|
write(f, join(JSON2.write.(users), "\n"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function loadusers(; file = expanduser("~/.config/beets/users.jsonl"))
|
function loadusers(; file = beetfile("users.jsonl"))
|
||||||
global users
|
global users
|
||||||
isfile(file) || touch(file)
|
isfile(file) || touch(file)
|
||||||
ps = JSON2.readlines(file)
|
ps = JSON2.readlines(file)
|
||||||
|
@ -42,3 +51,64 @@ function loadusers(; file = expanduser("~/.config/beets/users.jsonl"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function saveratings(; file = beetfile("ratings.json"))
|
||||||
|
global user_ratings
|
||||||
|
open(file, "w") do f
|
||||||
|
write(f, JSON2.write(user_ratings))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function savestarred(; file = beetfile("starred.json"))
|
||||||
|
global user_stars
|
||||||
|
open(file, "w") do f
|
||||||
|
write(f, JSON2.write(user_stars))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function loadratings(; file = beetfile("ratings.json"))
|
||||||
|
global user_ratings
|
||||||
|
isfile(file) || begin touch(file); return end
|
||||||
|
content = read(file, String)
|
||||||
|
isempty(content) && return file
|
||||||
|
user_ratings = JSON2.read(content, typeof(user_ratings))
|
||||||
|
end
|
||||||
|
|
||||||
|
function loadstarred(; file = beetfile("starred.json"))
|
||||||
|
global user_stars
|
||||||
|
isfile(file) || begin touch(file); return end
|
||||||
|
content = read(file, String)
|
||||||
|
isempty(content) && return user_stars
|
||||||
|
user_stars = JSON2.read(content, typeof(user_stars))
|
||||||
|
end
|
||||||
|
|
||||||
|
function config_create(;
|
||||||
|
cover_px_size = 250,
|
||||||
|
disk_cache_size = 5 * 1024 * 1024,
|
||||||
|
memory_cache_size = 1 * 1024 * 1024,
|
||||||
|
)
|
||||||
|
return Dict(:cover => Dict(:size => cover_px_size),
|
||||||
|
:cache => Dict(
|
||||||
|
:disk => Dict(
|
||||||
|
:size => disk_cache_size,
|
||||||
|
),
|
||||||
|
:memory => Dict(
|
||||||
|
:size => memory_cache_size,
|
||||||
|
)
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: those functions are perfect for a macro
|
||||||
|
function saveshared(; file = beetfile("shared.json"))
|
||||||
|
global shared
|
||||||
|
open(file, "w") do f
|
||||||
|
write(f, JSON2.write(shared))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function loadshared(; file = beetfile("shared.json"))
|
||||||
|
global shared
|
||||||
|
isfile(file) || return
|
||||||
|
content = read(file, String)
|
||||||
|
empty!(shared)
|
||||||
|
push!.(shared, JSON2.read(content, typeof(shared)))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,19 @@ upgrade_client() =
|
||||||
error(20, "Incompatible Subsonic REST protocol version. Client must upgrade.", 400)
|
error(20, "Incompatible Subsonic REST protocol version. Client must upgrade.", 400)
|
||||||
upgrade_server() =
|
upgrade_server() =
|
||||||
error(30, "Incompatible Subsonic REST protocol version. Server must upgrade.", 501)
|
error(30, "Incompatible Subsonic REST protocol version. Server must upgrade.", 501)
|
||||||
|
upgrade_server(m) =
|
||||||
|
error(30, "$m. Server must upgrade.", 501)
|
||||||
|
|
||||||
auth_failed() =
|
auth_failed() =
|
||||||
error(40, "Wrong username or password.", 401)
|
error(40, "Wrong username or password.", 401)
|
||||||
ldap_unsupported() =
|
ldap_unsupported() =
|
||||||
error(41, "Token authentication not supported for LDAP users.", 501)
|
error(41, "Token authentication not supported for LDAP users.", 501)
|
||||||
unuthorized() =
|
unauthorized() =
|
||||||
error(50, "User is not authorized for the given operation.", 401)
|
error(50, "User is not authorized for the given operation.", 401)
|
||||||
|
|
||||||
|
unauthorized(m) =
|
||||||
|
error(50, "User is not authorized for the given operation ($m).", 401)
|
||||||
|
|
||||||
not_found() =
|
not_found() =
|
||||||
error(70, "The requested data was not found.", 404)
|
error(70, "The requested data was not found.", 404)
|
||||||
not_found(n) =
|
not_found(n) =
|
||||||
|
@ -23,12 +28,12 @@ not_found(n) =
|
||||||
|
|
||||||
# FIXME: to use the status, we need to take the request dict
|
# FIXME: to use the status, we need to take the request dict
|
||||||
function error(code, message, error_code)
|
function error(code, message, error_code)
|
||||||
Mux.status(error_code)
|
|
||||||
(xdoc, xroot) = subsonic(version = "1.1.0", status = "failed")
|
(xdoc, xroot) = subsonic(version = "1.1.0", status = "failed")
|
||||||
er = new_child(xroot, "error")
|
er = new_child(xroot, "error")
|
||||||
set_attributes(er, [("code", string(code)),
|
set_attributes(er, [("code", string(code)),
|
||||||
("message", string(message))])
|
("message", string(message))])
|
||||||
doc_str = string(xdoc)
|
doc_str = string(xdoc)
|
||||||
free(xdoc)
|
free(xdoc)
|
||||||
return doc_str
|
return Dict(:status => error_code,
|
||||||
|
:body => doc_str)
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,14 +18,37 @@ mutable struct User
|
||||||
share::Bool
|
share::Bool
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct Playlist
|
# FIXME: add uuid format check
|
||||||
|
abstract type BeetUUID end
|
||||||
|
struct SongUUID <: BeetUUID
|
||||||
uuid::String
|
uuid::String
|
||||||
|
end
|
||||||
|
struct ArtistUUID <: BeetUUID
|
||||||
|
uuid::String
|
||||||
|
end
|
||||||
|
struct AlbumUUID <: BeetUUID
|
||||||
|
uuid::String
|
||||||
|
end
|
||||||
|
struct PlaylistUUID <: BeetUUID
|
||||||
|
uuid::String
|
||||||
|
end
|
||||||
|
|
||||||
|
convert(::Type{SongUUID}, s::String) = SongUUID(s)
|
||||||
|
convert(::Type{ArtistUUID}, s::String) = ArtistUUID(s)
|
||||||
|
convert(::Type{AlbumUUID}, s::String) = AlbumUUID(s)
|
||||||
|
convert(::Type{PlaylistUUID}, s::String) = PlaylistUUID(s)
|
||||||
|
convert(::Type{String}, s::BeetUUID) = s.uuid
|
||||||
|
|
||||||
|
import Beets.songbyid
|
||||||
|
songbyid(id::SongUUID) = songbyid(convert(String, id))
|
||||||
|
|
||||||
|
mutable struct Playlist
|
||||||
|
uuid::PlaylistUUID
|
||||||
name::String
|
name::String
|
||||||
comment::String
|
comment::String
|
||||||
owner::String
|
owner::String
|
||||||
public::Bool
|
public::Bool
|
||||||
# FIXME: replace with uuids only (and check they exists when importing)
|
songs::Vector{SongUUID}
|
||||||
songs::Vector{Song}
|
|
||||||
cover::String
|
cover::String
|
||||||
allowed::Vector{String}
|
allowed::Vector{String}
|
||||||
created::Dates.DateTime
|
created::Dates.DateTime
|
||||||
|
@ -36,7 +59,7 @@ function Playlist(owner::String
|
||||||
name = "New Playlist",
|
name = "New Playlist",
|
||||||
comment = "",
|
comment = "",
|
||||||
public = false,
|
public = false,
|
||||||
songs = Song[],
|
songs = SongUUID[],
|
||||||
cover = "",
|
cover = "",
|
||||||
allowed = String[],
|
allowed = String[],
|
||||||
creation = Dates.now())
|
creation = Dates.now())
|
||||||
|
@ -51,3 +74,40 @@ function User(name::String)
|
||||||
string(name, "@", domain),
|
string(name, "@", domain),
|
||||||
false, false, false, false, false, false, false, false, false)
|
false, false, false, false, false, false, false, false, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mutable struct Rating
|
||||||
|
item::Union{Artist,Album,Song}
|
||||||
|
rating::Int
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct Star
|
||||||
|
item::Union{Artist,Album,Song}
|
||||||
|
starred::Dates.DateTime
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct Share
|
||||||
|
id::UInt32
|
||||||
|
uuid::String
|
||||||
|
item::BeetUUID
|
||||||
|
username::String
|
||||||
|
description::String
|
||||||
|
created::Dates.DateTime
|
||||||
|
expires::Dates.DateTime
|
||||||
|
lastvisit::Union{Missing,Dates.DateTime}
|
||||||
|
count::Int
|
||||||
|
end
|
||||||
|
|
||||||
|
function Share(item::BeetUUID,
|
||||||
|
username::AbstractString,
|
||||||
|
description::AbstractString;
|
||||||
|
expires = Dates.now() + Dates.Day(7))
|
||||||
|
Share(Random.rand(UInt32),
|
||||||
|
Random.randstring(8),
|
||||||
|
item,
|
||||||
|
username, description,
|
||||||
|
# FIXME: do not hardcode
|
||||||
|
Dates.now(), expires,
|
||||||
|
missing, 0)
|
||||||
|
end
|
||||||
|
Share(i::BeetUUID, u::AbstractString, e::Dates.DateTime) = Share(i, u, ""; expires = e)
|
||||||
|
Share(i::BeetUUID, u::AbstractString) = Share(i, u, "")
|
||||||
|
|
|
@ -25,10 +25,10 @@ end
|
||||||
brokenartists(r) = map(x -> Beets.artist(x) , r.broken) |> unique
|
brokenartists(r) = map(x -> Beets.artist(x) , r.broken) |> unique
|
||||||
brokenalbums(r) = map(x -> Beets.album(x) , r.broken) |> unique
|
brokenalbums(r) = map(x -> Beets.album(x) , r.broken) |> unique
|
||||||
|
|
||||||
l() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(format_check(Beets.songs())))
|
l1() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(format_check(Beets.songs())))
|
||||||
m() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(existing_check(Beets.songs())))
|
m1() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(existing_check(Beets.songs())))
|
||||||
n() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(path_check(Beets.songs())))
|
n1() = map(a -> (artist = a.artist.name, title = a.title), brokenalbums(path_check(Beets.songs())))
|
||||||
|
|
||||||
l() |> DataFrame |> d -> FileIO.save("format.csv", d)
|
l1() |> DataFrame |> d -> FileIO.save("format.csv", d)
|
||||||
m() |> DataFrame |> d -> FileIO.save("missing.csv", d)
|
m1() |> DataFrame |> d -> FileIO.save("missing.csv", d)
|
||||||
n() |> DataFrame |> d -> FileIO.save("wrong_path.csv", d)
|
n1() |> DataFrame |> d -> FileIO.save("wrong_path.csv", d)
|
||||||
|
|
110
router.jl
110
router.jl
|
@ -6,6 +6,12 @@ function restpath!(target, req)
|
||||||
end
|
end
|
||||||
restp(p, app...) = branch(req -> restpath!(p, req), app...)
|
restp(p, app...) = branch(req -> restpath!(p, req), app...)
|
||||||
|
|
||||||
|
function share!(req)
|
||||||
|
return (length(req[:path]) == 2) &&
|
||||||
|
(req[:path][1] == "share")
|
||||||
|
end
|
||||||
|
share(app...) = branch(req -> share!(req), app...)
|
||||||
|
|
||||||
function torrentdl(query::AbstractString)
|
function torrentdl(query::AbstractString)
|
||||||
global rpc, me
|
global rpc, me
|
||||||
TransmissionRPC.getauth(rpc)
|
TransmissionRPC.getauth(rpc)
|
||||||
|
@ -18,7 +24,7 @@ function albumdl(query::AbstractString)
|
||||||
global rpc, me
|
global rpc, me
|
||||||
TransmissionRPC.getauth(rpc)
|
TransmissionRPC.getauth(rpc)
|
||||||
todl = RuTrackers.search(me, query)
|
todl = RuTrackers.search(me, query)
|
||||||
@show todl
|
# @show todl
|
||||||
lossless = RuTrackers.islossless.(todl)
|
lossless = RuTrackers.islossless.(todl)
|
||||||
discog = RuTrackers.isdiscography.(todl)
|
discog = RuTrackers.isdiscography.(todl)
|
||||||
m = findfirst(lossless .& .!discog)
|
m = findfirst(lossless .& .!discog)
|
||||||
|
@ -26,22 +32,118 @@ function albumdl(query::AbstractString)
|
||||||
TransmissionRPC.add(rpc, RuTrackers.download(me, todl[m]))
|
TransmissionRPC.add(rpc, RuTrackers.download(me, todl[m]))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function showshare(req)
|
||||||
|
notfound = Dict(:status => 404, :body => "Not found")
|
||||||
|
length(req[:path]) != 2 && return notfound
|
||||||
|
el = findfirst(x -> x.uuid == req[:path][2],
|
||||||
|
JlSonic.shared)
|
||||||
|
el == nothing && return notfound
|
||||||
|
sh = JlSonic.shared[el]
|
||||||
|
if JlSonic.expired(JlSonic.shared[el])
|
||||||
|
deleteat!(JlSonic.shared, el)
|
||||||
|
return notfound
|
||||||
|
end
|
||||||
|
sh.lastvisit = Dates.now()
|
||||||
|
query = split(req[:query], '/')
|
||||||
|
if query[1] == "dl"
|
||||||
|
sh.count += 1
|
||||||
|
if isa(sh.item, Beets.Song)
|
||||||
|
return JlSonic.sendfile(sh.item.path)
|
||||||
|
else
|
||||||
|
if length(query) == 2
|
||||||
|
el = parse(Int, query[2])
|
||||||
|
if 0 < el <= length(sh.item.songs)
|
||||||
|
return JlSonic.sendfile(sh.item.songs[el].path)
|
||||||
|
end
|
||||||
|
return JlSonic.upgrade_server("Not found")
|
||||||
|
else
|
||||||
|
return JlSonic.upgrade_server("Wrong request?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif query[1] == "stream"
|
||||||
|
sh.count += 1
|
||||||
|
if isa(sh.item, Beets.Song)
|
||||||
|
return JlSonic.giveconverted(sh.item.path, 192, "oga"; stream = true)
|
||||||
|
else
|
||||||
|
if length(query) == 2
|
||||||
|
el = parse(Int, query[2])
|
||||||
|
if 0 < el <= length(sh.item.songs)
|
||||||
|
return JlSonic.giveconverted(sh.item.songs[el].path, 192, "oga"; stream = true)
|
||||||
|
end
|
||||||
|
return JlSonic.upgrade_server("Not found")
|
||||||
|
else
|
||||||
|
return JlSonic.upgrade_server("Not implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
list = []
|
||||||
|
if isa(sh.item, Beets.Album)
|
||||||
|
push!(list, "It's an Album, here's the song list:")
|
||||||
|
for (n, s) in enumerate(sh.item.songs)
|
||||||
|
el = """
|
||||||
|
<a href="./$(sh.uuid)?stream/$n">stream $(s.title)</a>
|
||||||
|
<a href="./$(sh.uuid)?dl/$n" download="$(s.title).flac">download $(s.title)</a>
|
||||||
|
"""
|
||||||
|
push!(list, el)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
push!(list, """
|
||||||
|
<a href="./$(sh.uuid)?stream">stream</a>
|
||||||
|
<a href="./$(sh.uuid)?dl" download="$(sh.item.title).flac">download</a>
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
# application/octet-stream
|
||||||
|
return """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>$(sh.description)</title>
|
||||||
|
</head>
|
||||||
|
User: $(sh.username) shared <b>$(sh.item.title)</b> with you!<br/>
|
||||||
|
Description: $(sh.description)<br/>
|
||||||
|
Expires: $(sh.expires)<br/>
|
||||||
|
Viewed: $(sh.count) times<br/>
|
||||||
|
<br/>
|
||||||
|
$(join(list, "<br/>"))
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
config = JlSonic.config_create(disk_cache_size = 5 * 1024 * 1024,
|
||||||
|
memory_cache_size = 5 * 1024 * 1024)
|
||||||
|
|
||||||
dispatch = stack(
|
dispatch = stack(
|
||||||
# Browsing
|
# Browsing
|
||||||
restp("getMusicFolders", _ -> getMusicFolders()),
|
restp("getMusicFolders", _ -> getMusicFolders()),
|
||||||
restp("getMusicDirectory", req -> getMusicDirectory(req)),
|
restp("getMusicDirectory", req -> getMusicDirectory(req)),
|
||||||
|
restp("getAlbumList2", req -> getAlbumList2(req)),
|
||||||
restp("getAlbumList", req -> getAlbumList(req)),
|
restp("getAlbumList", req -> getAlbumList(req)),
|
||||||
restp("getGenres", _ -> getGenres()),
|
restp("getGenres", _ -> getGenres()),
|
||||||
restp("getArtists", _ -> getArtists()),
|
restp("getArtists", _ -> getArtists()),
|
||||||
restp("getArtist", r -> getArtist(r)),
|
restp("getArtist", r -> getArtist(r)),
|
||||||
restp("getAlbum", req -> getAlbum(req)),
|
restp("getAlbum", req -> getAlbum(req)),
|
||||||
restp("getSong", req -> getSong(req)),
|
restp("getSong", req -> getSong(req)),
|
||||||
|
restp("getLyrics", req -> getLyrics(req)),
|
||||||
|
restp("getShares", req -> getShares(req)),
|
||||||
|
restp("createShare", req -> createShare(req)),
|
||||||
|
restp("updateShare", req -> updateShare(req)),
|
||||||
|
restp("deleteShare", req -> deleteShare(req)),
|
||||||
# Album/song list
|
# Album/song list
|
||||||
restp("getRandomSongs", req -> getRandomSongs(req)),
|
restp("getRandomSongs", req -> getRandomSongs(req)),
|
||||||
|
restp("getStarred2", req -> getStarred2(req)),
|
||||||
|
restp("getStarred", req -> getStarred(req)),
|
||||||
|
restp("scrobble", req -> scrobble(req)),
|
||||||
|
restp("savePlayQueue", req -> savePlayQueue(req)),
|
||||||
|
restp("getPlayQueue", req -> getPlayQueue(req)),
|
||||||
|
restp("star", req -> star(req)),
|
||||||
|
restp("unstar", req -> unstar(req)),
|
||||||
|
restp("setRating", req -> setRating(req)),
|
||||||
# Searching
|
# Searching
|
||||||
restp("search3", req -> search3(req;
|
restp("search3", req -> search3(req;
|
||||||
dlalbum = albumdl,
|
dlalbum = albumdl,
|
||||||
dlall = torrentdl)),
|
dlall = torrentdl)),
|
||||||
|
restp("search", req -> search3(req;
|
||||||
|
dlalbum = albumdl,
|
||||||
|
dlall = torrentdl)),
|
||||||
# Playlists
|
# Playlists
|
||||||
restp("createPlaylist", req -> createPlaylist(req)),
|
restp("createPlaylist", req -> createPlaylist(req)),
|
||||||
restp("getPlaylists", req -> getPlaylists(req)),
|
restp("getPlaylists", req -> getPlaylists(req)),
|
||||||
|
@ -49,10 +151,12 @@ dispatch = stack(
|
||||||
restp("updatePlaylist", req -> updatePlaylist(req)),
|
restp("updatePlaylist", req -> updatePlaylist(req)),
|
||||||
restp("deletePlaylist", req -> deletePlaylist(req)),
|
restp("deletePlaylist", req -> deletePlaylist(req)),
|
||||||
# User management
|
# User management
|
||||||
|
restp("getUsers", req -> getUsers(req)),
|
||||||
restp("getUser", req -> getUser(req)),
|
restp("getUser", req -> getUser(req)),
|
||||||
|
restp("updateUser", req -> updateUser(req)),
|
||||||
# Media retrieval
|
# Media retrieval
|
||||||
restp("stream", req -> stream(req)),
|
restp("stream", req -> stream(req)),
|
||||||
restp("getCoverArt", req -> getCoverArt(req)),
|
restp("getCoverArt", req -> getCoverArt(req, config)),
|
||||||
# Media library scanning (can be used to check download status!)
|
# Media library scanning (can be used to check download status!)
|
||||||
# getScanStatus startScan
|
# getScanStatus startScan
|
||||||
)
|
)
|
||||||
|
|
47
server.jl
47
server.jl
|
@ -12,7 +12,7 @@ import TransmissionRPC
|
||||||
import JSON
|
import JSON
|
||||||
# FIXME: replace with JSON2 serialization
|
# FIXME: replace with JSON2 serialization
|
||||||
me = RuTrackers.RuTracker(read("rutracker.json", String) |> JSON.parse)
|
me = RuTrackers.RuTracker(read("rutracker.json", String) |> JSON.parse)
|
||||||
rpc = TransmissionRPC.Transmission(TransmissionRPC.Sockets.ip"192.168.1.3")
|
rpc = TransmissionRPC.Transmission(TransmissionRPC.Sockets.ip"192.168.1.4")
|
||||||
|
|
||||||
|
|
||||||
Beets.update_albums()
|
Beets.update_albums()
|
||||||
|
@ -21,6 +21,9 @@ push!(LOAD_PATH, realpath("JlSonic"))
|
||||||
using JlSonic
|
using JlSonic
|
||||||
JlSonic.loadplaylists()
|
JlSonic.loadplaylists()
|
||||||
JlSonic.loadusers()
|
JlSonic.loadusers()
|
||||||
|
JlSonic.loadratings()
|
||||||
|
JlSonic.loadstarred()
|
||||||
|
JlSonic.loadshared()
|
||||||
|
|
||||||
include("router.jl")
|
include("router.jl")
|
||||||
include("login.jl")
|
include("login.jl")
|
||||||
|
@ -30,25 +33,38 @@ if isdefined(Main, :logfile) && isopen(logfile)
|
||||||
close(logfile)
|
close(logfile)
|
||||||
end
|
end
|
||||||
|
|
||||||
logfile = open("requests.log", "a")
|
if !isdefined(:Main, :logfile)
|
||||||
|
logfile = open("requests.log", "a")
|
||||||
|
end
|
||||||
function logger(app, req)
|
function logger(app, req)
|
||||||
global logfile
|
global logfile
|
||||||
if isopen(logfile)
|
if !isopen(logfile)
|
||||||
logfile = open("requests.log", "a")
|
logfile = open("requests.log", "a")
|
||||||
end
|
end
|
||||||
write(logfile, string(req, "\n"))
|
ip = get(Dict(req[:headers]), "X-Real-IP", "0.0.0.0")
|
||||||
println(string("[", Dates.now(), "] ", req[:method], ": ", req[:path][end]))
|
pt = length(req[:path]) != 0 ? req[:path][end] : ""
|
||||||
#, " - ", req[:headers]["User-Agent"]))
|
content = if req[:login][:login]
|
||||||
return app(req)
|
string("[", Dates.now(), "] ",
|
||||||
|
"(", req[:login][:name], ") ",
|
||||||
|
req[:method], ": ", pt,
|
||||||
|
" (", ip, ")")
|
||||||
|
else
|
||||||
|
string("[", Dates.now(), "] ",
|
||||||
|
"(no auth) ",
|
||||||
|
req[:method], ": ", pt)
|
||||||
|
end
|
||||||
|
write(logfile, string(content, "\n"))
|
||||||
|
@info content
|
||||||
|
return app(req)
|
||||||
end
|
end
|
||||||
|
|
||||||
function basiccatch(app, req)
|
function basiccatch(app, req)
|
||||||
try
|
try
|
||||||
app(req)
|
app(req)
|
||||||
catch e
|
catch e
|
||||||
showerror(e, catch_backtrace())
|
showerror(e, catch_backtrace())
|
||||||
return d(:status => 500, :body => "failed")
|
return d(:status => 500, :body => "failed")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
defaults = stack(Mux.todict, basiccatch, Mux.splitquery, Mux.toresponse, Mux.assetserver, Mux.pkgfiles)
|
defaults = stack(Mux.todict, basiccatch, Mux.splitquery, Mux.toresponse, Mux.assetserver, Mux.pkgfiles)
|
||||||
@app sonic = (
|
@app sonic = (
|
||||||
|
@ -56,8 +72,9 @@ defaults = stack(Mux.todict, basiccatch, Mux.splitquery, Mux.toresponse, Mux.ass
|
||||||
# logger,
|
# logger,
|
||||||
restp("ping", _ -> ping()),
|
restp("ping", _ -> ping()),
|
||||||
restp("getLicense", _ -> getLicense()),
|
restp("getLicense", _ -> getLicense()),
|
||||||
mux(logger,
|
share(req -> showshare(req)),
|
||||||
sonic_login,
|
mux(sonic_login,
|
||||||
|
logger,
|
||||||
branch(req -> req[:login][:login],
|
branch(req -> req[:login][:login],
|
||||||
mux(dispatch, Mux.notfound())),
|
mux(dispatch, Mux.notfound())),
|
||||||
respond(auth_failed())),
|
respond(auth_failed())),
|
||||||
|
|
Loading…
Reference in New Issue