commit bd2826abdec4468ee706c8e8632bbc01f9b12103 Author: nixo Date: Fri May 17 20:22:01 2019 +0200 working diff --git a/JlSonic/JlSonic.jl b/JlSonic/JlSonic.jl new file mode 100644 index 0000000..b98cddb --- /dev/null +++ b/JlSonic/JlSonic.jl @@ -0,0 +1,41 @@ +__precompile__() +module JlSonic + +using Beets +using LightXML +import UUIDs +import HTTP + +const domain = "nixo.xyz" + +include("types.jl") +export Playlist, Album, Artist + +include("api.jl") +export ping, getLicense, + # Browsing + getMusicFolders, # getIndexes, getMusicDirectory, + getGenres, getArtists, getArtist, getAlbum, getSong, + # Album/song list + getAlbumList, getAlbumList2, getRandomSongs, + getSongsByGenre, getNowPlaying, getStarred, getStarred2, + # Searching + search3, + # Playlists + getPlaylists, getPlaylist, createPlaylist, + updatePlaylist, deletePlaylist, + # Media retrieval + stream, + # download, hls, getCaptions, + getCoverArt, # getLyrics, getAvatar, + # User management + getUser # getUsers, createUser, updateUser, deleteUser, changePassword + + +include("errors.jl") +export auth_failed + +include("beethelpers.jl") +include("beet2xml.jl") + +end # module JlSonic diff --git a/JlSonic/api.jl b/JlSonic/api.jl new file mode 100644 index 0000000..8901f2c --- /dev/null +++ b/JlSonic/api.jl @@ -0,0 +1,429 @@ +""" + Implementation of Subsonic APIs: http://www.subsonic.org/pages/api.jsp +""" + +"Subsonic API compatible version" +const apiversion = "1.16.1" +const domain = "nixo.xyz" + +""" +Helper function that prepares a subsonic response. +""" +function subsonic(; version = "1.10.1", status = "ok") + xdoc = XMLDocument() + xroot = create_root(xdoc, "subsonic-response") + set_attribute(xroot, "xmlns", "http://subsonic.org/restapi") + set_attribute(xroot, "status", status) + set_attribute(xroot, "version", version) + (xdoc, xroot) +end + +"Wrap a block inside the subsonic response to ease the free() of the XML" +macro subsonic(block) + return quote + (xdoc, xroot) = subsonic(version = $apiversion) + $block + doc_str = string(xdoc) + free(xdoc) + return doc_str + end +end + +function subsonic_return(doc) + doc_str = string(doc) + free(doc) + doc_str +end + +"Used to test connectivity with the server. Takes no extra parameters." +ping() = @subsonic(nothing) + +"Get details about the software license. Takes no extra parameters." +function getLicense() + @subsonic begin + set_attributes(new_child(xroot, "license"), + [ + ("valid", "true"), + ("email", string("admin@",domain)), + ("licenseExpires", "never"), + ("info", "This is juliaSonic, licensed under GPLv3+") + ]) + end +end + +"Returns all configured top-level music folders. Takes no extra parameters." +function getMusicFolders() + @subsonic begin + folders = new_child(xroot, "musicFolders") + folder = new_child(folders, "musicFolder") + set_attributes(folder, [("id", "1"), + ("name", "Music")]) + end +end + +"Returns all genres." +function getGenres() + @subsonic begin + songs = allsongs(); + res = Dict{String,Int}() + for genre in filter!(!isempty, getfield.(songs, :genre)) + t = get(res, genre, 0) + res[genre] = t+1 + end + genres = new_child(xroot, "genres") + for k in keys(res) + genre = new_child(genres, "genre") + set_attributes(genre, [ + ("songCount", string(res[k])), + # FIXME + ("albumCount", "0"), + ]) + add_text(genre, k) + end + end +end + +"Similar to getIndexes, but organizes music according to ID3 tags." +function getArtists() + # TODO + (xdoc, xroot) = subsonic(version = "1.12.0") + indexes = new_child(xroot, "artists") + set_attribute(indexes, "ignoredArticles", "") + beetsdb = Beets.getartists() + artists = unique(beetsdb) + # albums = group_albums_as_artists() + # .|> does not work in a macro. What to do? + for index in unique(first.(getfield.(artists, Ref(:name))) .|> uppercase) + indexXML = new_child(indexes, "index") + set_attribute(indexXML, "name", string(index)) + for artist in filter(x -> startswith(x.name, string(index)), artists) + artistXML = new_child(indexXML, "artist") + set_attributes(artistXML, + [("name", artist.name), + ("id", artist.uuid), + ("coverArt", ""), + ("albumCount", "")]) + end + end + doc_str = string(xdoc) + free(xdoc) + return doc_str +end + +"""Returns details for an artist, including a list of albums. + +This method organizes music according to ID3 tags.""" +function getArtist(id::String) + artists = Beets.getartists() + matching = artists[getfield.(artists, :uuid) .== id] + name = length(matching) > 0 ? first(matching).name : "" + isempty(name) && return not_found() + # Create the response + (xdoc, xroot) = subsonic() + artistXML = new_child(xroot, "artist") + artist = first(matching) + artist_albums = [i for i in Beets.getalbums() if i.artist == artist] + set_attributes(artistXML, [ + ("id", artist.uuid), + ("albumCount", string(length(artist_albums))), + ("name", artist.name), + ("coverArt", "false") + ]) + for album in artist_albums + push!(xroot, album) + end + return subsonic_return(xdoc) +end + +function getArtist(req::Dict) + query = HTTP.URIs.queryparams(req[:query]) + artistid = get(query, "id", "") + isempty(artistid) && return missing_parameter("id") + return getArtist(string(artistid)) +end + +function getAlbum(req::Dict) + query = HTTP.URIs.queryparams(req[:query]) + albumid = get(query, "id", "") + isempty(albumid) && return missing_parameter("id") + return getAlbum(albumid) +end + +function getAlbumList(req::Dict) + query = HTTP.URIs.queryparams(req[:query]) + @show query + albumtype = get(query, "type", "") + isempty(albumtype) && return missing_parameter("type") + @subsonic begin + list = new_child(xroot, "albumList") + push!.(Ref(list), Beets.getalbums()) + end +end + +function getAlbum(albumid) + matching = [album for album in Beets.getalbums() if album.uuid == albumid] + if length(matching) < 1 + return not_found("album") + end + album = first(matching) + (xdoc, xroot) = subsonic() + albumXML = push!(xroot, album) + push!(albumXML, [(s, album) for s in album.songs]) + return subsonic_return(xdoc) +end + +function getSong(req) + query = HTTP.URIs.queryparams(req[:query]) + id = get(query, "id", "") + isempty(id) && return missing_parameter() + matching = [album for album in Beets.getalbums() + if any(getfield.(album.songs, :uuid) .== id)] + length(matching) == 0 && return not_found("song") + (xdoc, xroot) = subsonic() + push!(xroot, (first(filter(s -> s.uuid == id, first(matching).songs)), + first(matching))) + return subsonic_return(xdoc) +end + +import Random +function getRandomSongs(; size = 10, + genre::Union{Missing,String} = missing, + fromYear::Union{Missing,Int} = missing, + toYear::Union{Missing,Int} = missing, + musicFolderId::Union{Missing,String} = missing) + songs = allsongs(); + # Randomize + songs = songs[Random.randperm(length(songs))]; + # Filter + !ismissing(genre) && filter!(x -> strip(x.genre) == strip(genre), songs) + !ismissing(fromYear) && filter!(x -> x.year > fromYear, songs) + !ismissing(toYear) && filter!(x -> x.year < toYear, songs) + # Take size + songs = length(songs) > size ? songs[1:size] : songs; + # Create output + (xdoc, xroot) = subsonic() + list = new_child(xroot, "randomSongs") + albums = Beets.getalbums(); + for song in songs + album = filter(x -> song in x.songs, albums) |> first + push!(list, (song, album)) + end + return subsonic_return(xdoc) +end + +function getRandomSongs(req) + query = HTTP.URIs.queryparams(req[:query]) + # name Required Default Notes + # size No 10 The maximum number of songs to return. Max 500. + size = parse(Int, get(query, "size", "10")) + size = size > 500 ? 500 : size + # genre No Only returns songs belonging to this genre. + genre = get(query, "genre", missing) + # fromYear No Only return songs published after or in this year. + # toYear No Only return songs published before or in this year. + fy = get(query, "fromYear", missing) + fromY = ismissing(fy) ? missing : parse(Int, fy) + ty = get(query, "toYear", missing) + toY = ismissing(ty) ? missing : parse(Int, ty) + getRandomSongs(size = size, + genre = ismissing(genre) ? missing : string(genre), + fromYear = fromY, toYear = toY) +end + + + +function match(q::Regex, album::Beets.Album) + (match(q, album.title) !== nothing) || (match(q, album.artist.name) !== nothing) || + any(map(s -> match(q, s.title) !== nothing, album.songs)) +end + +function makequery(q::String) + nq = replace(lowercase(q), "*" => ".*") + nq = string(".*", nq) + return Regex(nq) +end + +function search3(req) + query = HTTP.URIs.queryparams(req[:query]) + q = get(query, "query", "") + isempty(q) && return missing_parameter("query") + if length(q) > 1 && q[1] == '!' + @info "This is the special torrent mode!" + end + songCount = parse(Int, get(query, "songCount", "20")) + # TODO: + # artistCount No 20 Maximum number of artists to return. + # artistOffset No 0 Search result offset for artists. Used for paging. + # albumCount No 20 Maximum number of albums to return. + # albumOffset No 0 Search result offset for albums. Used for paging. + # songOffset No 0 Search result offset for songs. Used for paging. + # musicFolderId No (Since 1.12.0) Only return results from music folder with the given ID. See getMusicFolders. + (xdoc, xroot) = subsonic() + results = new_child(xroot, "searchResult3") + k = makequery(string(q)) + matchingartists = Beets.getartists() + filter!(a -> Base.match(k, lowercase(a.name)) !== nothing, matchingartists) + albums = Beets.getalbums(); + matchingalbums = filter(a -> Base.match(k, lowercase(a.title)) !== nothing, + albums) + matchingsongs = Tuple{Beets.Song,Beets.Album}[] + for album in albums, song in album.songs + if Base.match(k, lowercase(song.title)) !== nothing + push!(matchingsongs, (song, album)) + end + end + # Artists + push!.(Ref(results), matchingartists) + # # Albums + push!.(Ref(results), matchingalbums) + # Songs + push!(results, matchingsongs) + 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 + #= + Parameter Required Default Comment + playlistId Yes (if updating) The playlist ID. + name Yes (if creating) The human-readable name of the playlist. + songId No ID of a song in the playlist. Use one songId parameter for each song in the playlist. + =# + query = HTTP.URIs.queryparams(req[:query]) + playlistId = get(query, "playlistId", "") + name = get(query, "name", "") + songId = get(query, "songId", "") + # Check required params + isempty(songId) && return missing_parameter("songId") + songs = allsongs(); + songn = findfirst(s -> s.uuid == songId, songs) + songn === nothing && return not_found("songId") + song = songs[songn] + if !isempty(playlistId) + if playlistId in keys(user_playlists) + push!(playlist, song) + else + return not_found("playlistId") + end + elseif !isempty(name) + playlist = Playlist(req[:login][:name], name = name) + push!(playlist, song) + else + return missing_parameter("either name or playlistId") + end + push!(user_playlists, playlist) + # Return the playlist + (xdoc, xroot) = subsonic() + push!(xroot, playlist) + return subsonic_return(xdoc) +end + +"Returns all playlists a user is allowed to play." +function getPlaylists(req) + global user_playlists + user = req[:login][:name] + # FIXME: add support for admin (ask other user's playlists, v 1.8.0) + (xdoc, xroot) = subsonic(version = "1.0.0") + playlistsXML = new_child(xroot, "playlists") + for playlist in filter(p -> p.owner == user, user_playlists) + push!(playlistsXML, playlist) + end + return subsonic_return(xdoc) +end + +"Returns a listing of files in a saved playlist." +function getPlaylist(req) + global user_playlists + + query = HTTP.URIs.queryparams(req[:query]) + id = get(query, "id", "") + isempty(id) && return missing_parameter("id") + + m = findfirst(x -> x.owner == req[:login][:name], user_playlists) + m == nothing && return not_found("id") + + (xdoc, xroot) = subsonic() + append!(xroot, user_playlists[m]) + return subsonic_return(xroot) +end + +"Updates a playlist. Only the owner of a playlist is allowed to update it." +function updatePlaylist(req) + global user_playlists + query = HTTP.URIs.queryparams(req[:query]) + @show query + playlistId = get(query, "playlistId", "") + isempty(playlistId) && return missing_parameter("playlistId") + # FIXME: check ownership + pn = findfirst(p -> p.uuid == playlistId, + user_playlists) + pn == nothing && return not_found("playlistId") + playlist = user_playlists[pn] + 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", "") + songIndexToRemove = isempty(IndexToRemove) ? -1 : parse(Int,IndexToRemove) + 1 + + # TODO: Support multiple (repeated) parameter + if !isempty(songIdAdd) + songs = allsongs(); + songn = findfirst(s -> s.uuid == songIdAdd, songs) + songn === nothing && return not_found("songIdToAdd") + song = songs[songn] + push!(playlist, song) + end + # TODO: Support multiple (repeated) parameter + if songIndexToRemove > 0 && songIndexToRemove <= length(playlist.songs) + deleteat!(playlist.songs, songIndexToRemove) + end + @subsonic(nothing) +end + +"Deletes a saved playlist." +function deletePlaylist(req) + global user_playlists + query = HTTP.URIs.queryparams(req[:query]) + id = get(query, "id", "") + isempty(id) && return missing_parameter("id") + # FIXME: check ownership + filter!(p -> p.uuid != id, user_playlists) + @subsonic(nothing) +end + +function getUser(req) + (xdoc, xroot) = subsonic() + push!(xroot, User(req[:login][:name])) + return subsonic_return(xroot) +end + +# Media retriveal + +"Returns a cover art image." +function getCoverArt(req::Dict) + query = HTTP.URIs.queryparams(req[:query]) + id = get(query, "id", "") + isempty(id) && return missing_parameter("id") + albums = Beets.getalbums() + n = findfirst(a -> album.uuid == id, albums) + n === nothing && return not_found("id") + # @show matching.cover + return Dict(:body => read(albums)) +end + +"Streams a given media file." +function stream(req::Dict) + query = HTTP.URIs.queryparams(req[:query]) + id = get(query, "id", "") + isempty(id) && return missing_parameter("id") + songs = allsongs() + m = findfirst(x -> (x.uuid == id), songs) + m === nothing && return not_found("id") + return Dict(:body => read(songs[m].path)) +end diff --git a/JlSonic/beet2xml.jl b/JlSonic/beet2xml.jl new file mode 100644 index 0000000..8dc710b --- /dev/null +++ b/JlSonic/beet2xml.jl @@ -0,0 +1,142 @@ +import Base.push! + +function push!(root::XMLElement, p::Playlist) + playlistXML = new_child(root, "playlist") + set_attributes(playlistXML, [ + ("id", p.uuid), + ("name", p.name), + ("comment", p.comment), + ("owner", p.owner), + ("public", string(p.public)), + ("songCount", string(length(p.songs))), + ("duration", reduce(+, p.songs, init = 0.0) |> floor |> string), + ("created", "FIXME"), + ("coverArt", p.cover), + ]) + playlistXML + #= + sindre + john + =# +end + +function append!(root::XMLElement, p::Playlist) + playlistXML = push!(root, p) + # FIXME: 1. allowed users + for song in p.songs + entry = new_child(playlistXML, "entry") + set_attributes(entry, props(song)) + end + playlistXML +end + +push!(p::Playlist, s::Song) = push!(p.songs, s) + +function push!(root::XMLElement, album::Beets.Album) + albumXML = new_child(root, "album") + set_attributes(albumXML, [ + ("name", album.title), + ("id", album.uuid), + ("name", album.artist.name), + ("artistId", album.artist.uuid), + ("artist", album.artist.name), + # FIXME + ("created", "0"), + ("coverArt", album.uuid), + ("songs", "FIXME"), + ("songCount", string(length(album.songs))), + ("duration", string(sum([t.length for t in album.songs]))) + ]) + albumXML +end + +function push!(root::XMLElement, artist::Beets.Artist) + artistXML = new_child(root, "artist") + set_attributes(artistXML, [ + ("id", artist.uuid), + ("name", artist.name), + ("coverArt", artist.uuid), + ("albumCount", "0") + ]) + artistXML +end + +function push!(root::XMLElement, song_album::Tuple{Beets.Song,Beets.Album}) + songXML = new_child(root, "song") + song, album = song_album + set_attributes(songXML, [ + ("id", song.uuid), + # ("parent", album.artist.uuid), # Not clear + ("title", song.title), + ("album", album.title), + ("artist", album.artist.name), + ("isDir", "false"), + ("coverArt", album.uuid), + ("created", "FIXME"), + ("duration", string(floor(song.length) |> Int)), + ("bitrate", string(song.bitrate)), + ("size", string(filesize(song.path))), + ("suffix", lowercase(song.format)), + ## FIXME + ("contentType", "audio/flac"), # mpeg + ("isVideo", "false"), + ("path", relpath(song.path, Beets.musicdir())), + ("albumId", album.uuid), + ("artistId", album.artist.uuid), + ("type", "music") + ]) + root +end + +function props(song::Song) + [ + ("id", song.uuid), + # ("parent", album.artist.uuid), # Not clear + ("title", song.title), + ("album", song.title), + # ("artist", song.album.artist.name), + ("isDir", "false"), + ("coverArt", song.uuid), + ("created", "FIXME"), + ("duration", string(floor(song.length) |> Int)), + ("bitrate", string(song.bitrate)), + ("size", string(filesize(song.path))), + ("suffix", lowercase(song.format)), + ## FIXME + ("contentType", "audio/flac"), # mpeg + ("isVideo", "false"), + ("path", relpath(song.path, Beets.musicdir())), + # ("albumId", song.album.uuid), + # ("artistId", song.album.artist.uuid), + ("type", "music") + ] +end + +function push!(root::XMLElement, songs::Vector{Tuple{Beets.Song,Beets.Album}}) + for (song, album) in songs + songXML = new_child(root, "song") + set_attributes(songXML, props(song)) + end + root +end + +function push!(root::XMLElement, user::User) + userXML = new_child(root, "user") + set_attributes(userXML, + [ + ("username", user.name) + ("email", user.email) + ("adminRole", string(user.admin)) + ("scrobblingEnabled", "false") + ("settingsRole", string(user.settings)) + ("downloadRole", string(user.download)) + ("uploadRole", string(user.upload)) + ("playlistRole", string(user.playlist)) + ("coverArtRole", string(user.cover)) + ("commentRole", string(user.comment)) + ("podcastRole", "false") + ("streamRole", string(user.stream)) + ("jukeboxRole", "false") + ("shareRole", string(user.share)) + ]) +end diff --git a/JlSonic/beethelpers.jl b/JlSonic/beethelpers.jl new file mode 100644 index 0000000..6ccaa13 --- /dev/null +++ b/JlSonic/beethelpers.jl @@ -0,0 +1,4 @@ +function allsongs() + albums = [album.songs for album in Beets.getalbums()]; + songs = Iterators.flatten(albums) |> collect; +end diff --git a/JlSonic/errors.jl b/JlSonic/errors.jl new file mode 100644 index 0000000..1e87787 --- /dev/null +++ b/JlSonic/errors.jl @@ -0,0 +1,34 @@ +import Mux +# https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +error() = error(0, "A generic error.", 500) +missing_parameter() = error(10, "Required parameter is missing.", 400) +missing_parameter(n) = error(10, "Required parameter ($n) is missing.", 400) + +upgrade_client() = + error(20, "Incompatible Subsonic REST protocol version. Client must upgrade.", 400) +upgrade_server() = + error(30, "Incompatible Subsonic REST protocol version. Server must upgrade.", 501) + +auth_failed() = + error(40, "Wrong username or password.", 401) +ldap_unsupported() = + error(41, "Token authentication not supported for LDAP users.", 501) +unuthorized() = + error(50, "User is not authorized for the given operation.", 401) + +not_found() = + error(70, "The requested data was not found.", 404) +not_found(n) = + error(70, "The requested data ($n) was not found.", 404) + +# FIXME: to use the status, we need to take the request dict +function error(code, message, error_code) + Mux.status(error_code) + (xdoc, xroot) = subsonic(version = "1.1.0", status = "failed") + er = new_child(xroot, "error") + set_attributes(er, [("code", string(code)), + ("message", string(message))]) + doc_str = string(xdoc) + free(xdoc) + return doc_str +end diff --git a/JlSonic/types.jl b/JlSonic/types.jl new file mode 100644 index 0000000..eadf7a4 --- /dev/null +++ b/JlSonic/types.jl @@ -0,0 +1,41 @@ +mutable struct Playlist + uuid::String + name::String + comment::String + owner::String + public::Bool + songs::Vector{Song} + cover::String +end + +mutable struct User + name::String + email::String + # scrobbling::Bool + admin::Bool + settings::Bool + download::Bool + upload::Bool + cover::Bool + playlist::Bool + comment::Bool + # podcast::Bool + stream::Bool + # jukebox::Bool + share::Bool +end + +function Playlist(owner::String + ; uuid = string(UUIDs.uuid4()), + name = "New Playlist", + comment = "", + public = false, + songs = Song[], + cover = "",) + Playlist(uuid, name, comment, owner, public, songs, cover) +end + +function User(name::String) + User(name, string(name, "@", domain), + false, false, false, false, false, false, false, false, false) +end diff --git a/login.jl b/login.jl new file mode 100644 index 0000000..5d259b2 --- /dev/null +++ b/login.jl @@ -0,0 +1,39 @@ +using MD5 +using HTTP +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, + :login => false) + return app(req) +end + +function checkpassword(app, req) + # FIXME: do not hardcode the password here! + password = "test" + # @show bytes2hex(MD5.md5(string(password, salt))) + # @show req[:login][:token] + # @show req[:login][:salt] + if !isempty(req[:login][:salt]) + if bytes2hex(MD5.md5(string(password, req[:login][:salt]))) == + req[:login][:token] + req[:login][:login] = true + end + elseif !isempty(req[:login][:password]) + if startswith(req[:login][:password], "enc:") + req[:login][:login] = + String(hex2bytes(split(req[:login][:password], ":")[2])) == password + else + req[:login][:login] = password == req[:login][:password] + end + end + return app(req) +end + +sonic_login = stack(getlogin, checkpassword) diff --git a/router.jl b/router.jl new file mode 100644 index 0000000..b7c5841 --- /dev/null +++ b/router.jl @@ -0,0 +1,36 @@ +function restpath!(target, req) + @show req[:path] + length(req[:path]) < 2 && return false + return req[:path][1] == "rest" && + startswith(req[:path][2], target) +end +restp(p, app...) = branch(req -> restpath!(p, req), app...) + +dispatch = stack( + # Browsing + restp("getMusicFolders", _ -> getMusicFolders()), + restp("getMusicDirectory", req -> getmusicdirectory(req)), + restp("getAlbumList", req -> getAlbumList(req)), + restp("getGenres", _ -> getGenres()), + restp("getArtists", _ -> getArtists()), + restp("getArtist", r -> getArtist(r)), + restp("getAlbum", req -> getAlbum(req)), + restp("getSong", req -> getSong(req)), + # Album/song list + restp("getRandomSongs", req -> getRandomSongs(req)), + # Searching + restp("search3", req -> search3(req)), + # Playlists + restp("createPlaylist", req -> createPlaylist(req)), + restp("getPlaylists", req -> getPlaylists(req)), + restp("getPlaylist", req -> getPlaylist(req)), + restp("updatePlaylist", req -> updatePlaylist(req)), + restp("deletePlaylist", req -> deletePlaylist(req)), + # User management + restp("getUser", req -> getUser(req)), + # Media retrieval + restp("stream", req -> stream(req)), + restp("getCoverArt", req -> getCoverArt(req)), + # Media library scanning (can be used to check download status!) + # getScanStatus startScan +) diff --git a/server.jl b/server.jl new file mode 100644 index 0000000..8441db2 --- /dev/null +++ b/server.jl @@ -0,0 +1,23 @@ +using Mux +using HTTP +using Revise + +import Beets +push!(LOAD_PATH, realpath("JlSonic")) +using JlSonic + +include("router.jl") +include("login.jl") +@app sonic = ( + Mux.defaults, + restp("ping", _ -> ping()), + restp("getLicense", _ -> getLicense()), + mux(sonic_login, + branch(req -> req[:login][:login], + mux(dispatch, Mux.notfound())), + respond(auth_failed())), +) +if !isdefined(Main, :started) + serve(sonic) + started = true +end