├── .gitignore
├── .travis.yml
├── README.md
├── deploy_rsa.enc
├── docker-compose.yml
├── elm
├── .gitignore
├── .travis.yml
├── .vscode
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── README.md
├── dist
│ └── config.js
├── elm.json
├── index.html
├── index.js
├── package.json
├── review
│ ├── ReviewConfig.elm
│ └── elm.json
├── src
│ ├── Album
│ │ ├── Fetch.elm
│ │ ├── Select.elm
│ │ ├── Types.elm
│ │ └── Update.elm
│ ├── Artist
│ │ ├── Fetch.elm
│ │ ├── Select.elm
│ │ └── Types.elm
│ ├── Audio
│ │ ├── Actions.elm
│ │ ├── Model.elm
│ │ ├── Msg.elm
│ │ ├── Request.elm
│ │ ├── Select.elm
│ │ ├── State.elm
│ │ └── Update.elm
│ ├── Cache.elm
│ ├── Config.elm
│ ├── DTO
│ │ ├── Authenticate.elm
│ │ ├── Credentials.elm
│ │ └── Ticket.elm
│ ├── Entities
│ │ ├── Album.elm
│ │ ├── AlbumSummary.elm
│ │ ├── Artist.elm
│ │ ├── ArtistSummary.elm
│ │ ├── Playlist.elm
│ │ ├── PlaylistSummary.elm
│ │ └── SongSummary.elm
│ ├── Loadable.elm
│ ├── Main.elm
│ ├── Model.elm
│ ├── Msg.elm
│ ├── Nexus
│ │ ├── Callback.elm
│ │ ├── Fetch.elm
│ │ └── Model.elm
│ ├── Player
│ │ ├── Actions.elm
│ │ ├── Model.elm
│ │ ├── Msg.elm
│ │ ├── Repeat.elm
│ │ ├── Select.elm
│ │ └── Update.elm
│ ├── Playlist
│ │ ├── Fetch.elm
│ │ ├── Select.elm
│ │ ├── Types.elm
│ │ └── Update.elm
│ ├── Ports.elm
│ ├── Rest
│ │ └── Core.elm
│ ├── Routing.elm
│ ├── Socket
│ │ ├── Actions.elm
│ │ ├── Core.elm
│ │ ├── DTO
│ │ │ ├── Album.elm
│ │ │ ├── AlbumSummary.elm
│ │ │ ├── Artist.elm
│ │ │ ├── ArtistSummary.elm
│ │ │ ├── Playlist.elm
│ │ │ └── SongSummary.elm
│ │ ├── Listener.elm
│ │ ├── Listeners
│ │ │ └── ScanStatus.elm
│ │ ├── Message.elm
│ │ ├── MessageId.elm
│ │ ├── Methods
│ │ │ ├── GetAlbums.elm
│ │ │ ├── GetArtists.elm
│ │ │ ├── GetPlaylists.elm
│ │ │ ├── Handshake.elm
│ │ │ ├── Start.elm
│ │ │ └── StartScan.elm
│ │ ├── Model.elm
│ │ ├── Notification.elm
│ │ ├── NotificationListener.elm
│ │ ├── Request.elm
│ │ ├── RequestData.elm
│ │ ├── Response.elm
│ │ ├── Select.elm
│ │ ├── SocketMsg.elm
│ │ └── Update.elm
│ ├── Song
│ │ ├── Select.elm
│ │ └── Types.elm
│ ├── Types.elm
│ ├── Updaters.elm
│ ├── Util.elm
│ ├── Views
│ │ ├── Album.elm
│ │ ├── Albums.elm
│ │ ├── Artist.elm
│ │ ├── Artists.elm
│ │ ├── Home.elm
│ │ ├── Login.elm
│ │ ├── MiniAlbum.elm
│ │ ├── Player.elm
│ │ ├── Playlist.elm
│ │ ├── PlaylistItem.elm
│ │ ├── Playlists.elm
│ │ ├── Root.elm
│ │ ├── Sidebar.elm
│ │ └── Song.elm
│ ├── css
│ │ └── reset.css
│ └── sass
│ │ ├── _album.scss
│ │ ├── _app.scss
│ │ ├── _home.scss
│ │ ├── _login.scss
│ │ ├── _player.scss
│ │ ├── _playlist.scss
│ │ ├── artist.scss
│ │ └── styles.scss
├── webpack.common.js
├── webpack.dev.js
├── webpack.home.js
├── webpack.live.js
├── webpack.prod.js
└── yarn.lock
├── go
├── .gitignore
├── .vscode
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── api
│ ├── api
│ │ ├── api.go
│ │ ├── handlerFactory.go
│ │ ├── response.go
│ │ └── spa.go
│ ├── controller
│ │ ├── art.go
│ │ ├── authenticate.go
│ │ ├── authenticate_test.go
│ │ ├── stream.go
│ │ └── ticket.go
│ └── dto
│ │ ├── credentials.go
│ │ ├── ticket.go
│ │ └── token.go
├── config.yaml
├── config
│ ├── config.go
│ └── config_test.go
├── dal
│ ├── dal.go
│ ├── dal_test.go
│ └── mock.go
├── dao
│ ├── album.go
│ ├── albumList2Type.go
│ ├── art.go
│ ├── artist.go
│ ├── errNotFound.go
│ ├── genre.go
│ ├── playlist.go
│ ├── playlistEntry.go
│ └── song.go
├── database
│ ├── default.go
│ ├── default_test.go
│ └── mock.go
├── egs
│ ├── getAlbum.json
│ ├── getAlbumList2.json
│ ├── getArtists.json
│ ├── getIndexes.json
│ ├── getMusicDirectory.json
│ ├── getPlaylist.xml
│ ├── getSongsByGenre.json
│ └── getplaylists.json
├── entities
│ └── fileData.go
├── go.mod
├── go.sum
├── hasher
│ └── hasher.go
├── linux_dsm_build.sh
├── log-config.xml
├── main.go
├── migrations
│ ├── 000001_initial.down.sql
│ ├── 000001_intial.up.sql
│ ├── 000002_add_playlist_owner.down.sql
│ └── 000002_add_playlist_owner.up.sql
├── projectpath
│ └── Root.go
├── provider
│ ├── beetsProvider.go
│ ├── beetsProvider_test.go
│ ├── fsProvider.go
│ ├── mockProvider.go
│ ├── provider.go
│ ├── scanner.go
│ └── scanner_test.go
├── services
│ ├── authenticator.go
│ ├── authenticator_test.go
│ ├── clock.go
│ ├── fileScanner.go
│ ├── fileScanner_test.go
│ ├── resizer.go
│ ├── tagger.go
│ └── tagger_test.go
├── socket
│ ├── client.go
│ ├── connection.go
│ ├── dto
│ │ ├── album.go
│ │ ├── albumCollection.go
│ │ ├── albumSummary.go
│ │ ├── artist.go
│ │ ├── artistCollection.go
│ │ ├── notification.go
│ │ ├── playlist.go
│ │ ├── playlistCollection.go
│ │ ├── request.go
│ │ ├── response.go
│ │ ├── songSummary.go
│ │ └── ticketResponse.go
│ ├── handler.go
│ ├── handlers
│ │ ├── getAlbum.go
│ │ ├── getAlbums.go
│ │ ├── getArtist.go
│ │ ├── getArtists.go
│ │ ├── getPlaylist.go
│ │ ├── getPlaylists.go
│ │ └── startScan.go
│ ├── hub.go
│ ├── mockHub.go
│ └── ticketer.go
├── sound.conf
├── subsonic
│ ├── api
│ │ ├── api.go
│ │ ├── format.go
│ │ ├── handlerFactory.go
│ │ ├── parse.go
│ │ ├── parse_test.go
│ │ ├── response.go
│ │ ├── serialiser.go
│ │ └── serialiser_test.go
│ ├── dto
│ │ ├── album.go
│ │ ├── albumDirectory.go
│ │ ├── albumList2.go
│ │ ├── albumWithSongs.go
│ │ ├── album_test.go
│ │ ├── artist.go
│ │ ├── artistCollection.go
│ │ ├── artistCollection_test.go
│ │ ├── artistDirectory.go
│ │ ├── artistWithAlbums.go
│ │ ├── artist_test.go
│ │ ├── data_test.go
│ │ ├── directory.go
│ │ ├── directoryID.go
│ │ ├── error.go
│ │ ├── genres.go
│ │ ├── genres_test.go
│ │ ├── license.go
│ │ ├── musicFolderCollection.go
│ │ ├── playlist.go
│ │ ├── playlistCollection.go
│ │ ├── playlistCollection_test.go
│ │ ├── playlist_test.go
│ │ ├── randomSongs.go
│ │ ├── randomSongs_test.go
│ │ ├── scanStatus.go
│ │ ├── scanStatus_test.go
│ │ ├── search.go
│ │ ├── search_test.go
│ │ ├── song.go
│ │ ├── songDirectory.go
│ │ ├── song_test.go
│ │ ├── songsByGenre.go
│ │ ├── user.go
│ │ ├── userCollection.go
│ │ └── user_test.go
│ ├── handler
│ │ ├── createPlaylist.go
│ │ ├── createPlaylist_test.go
│ │ ├── deletePlaylist.go
│ │ ├── deletePlaylist_test.go
│ │ ├── download.go
│ │ ├── getAlbum.go
│ │ ├── getAlbumList2.go
│ │ ├── getAlbum_test.go
│ │ ├── getArtist.go
│ │ ├── getArtist_test.go
│ │ ├── getArtists.go
│ │ ├── getArtists_test.go
│ │ ├── getCoverArt.go
│ │ ├── getGenres.go
│ │ ├── getIndexes.go
│ │ ├── getLicense.go
│ │ ├── getMusicDirectory.go
│ │ ├── getMusicDirectory_test.go
│ │ ├── getMusicFolders.go
│ │ ├── getPlaylist.go
│ │ ├── getPlaylists.go
│ │ ├── getRandomSongs.go
│ │ ├── getRandomSongs_test.go
│ │ ├── getSong.go
│ │ ├── getSong_test.go
│ │ ├── getSongsByGenre.go
│ │ ├── getSongsByGenre_test.go
│ │ ├── getUser.go
│ │ ├── getUsers.go
│ │ ├── ping.go
│ │ ├── ping_test.go
│ │ ├── scan.go
│ │ ├── search.go
│ │ ├── search_test.go
│ │ ├── star.go
│ │ ├── stream.go
│ │ ├── updatePlaylist.go
│ │ └── updatePlaylist_test.go
│ └── routes
│ │ └── routes.go
├── testdata
│ ├── beetslib.blb
│ ├── config.yaml
│ ├── dao
│ │ ├── albums.yml
│ │ ├── artists.yml
│ │ ├── arts.yml
│ │ ├── genres.yml
│ │ ├── playlist_entries.yml
│ │ ├── playlists.yml
│ │ └── songs.yml
│ └── music
│ │ ├── 1.mp3
│ │ ├── 2.mp3
│ │ ├── lost.txt
│ │ └── subfolder
│ │ └── 3.mp3
├── todo.txt
└── util
│ ├── util.go
│ └── util_test.go
└── sound.code-workspace
/.gitignore:
--------------------------------------------------------------------------------
1 | .data/**/*
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | addons:
2 | ssh_known_hosts: hednowley.synology.me:169
3 |
4 | services:
5 | - postgresql
6 | - docker
7 |
8 | before_deploy:
9 | - openssl aes-256-cbc -K $encrypted_8c047e66c105_key -iv $encrypted_8c047e66c105_iv -in ../deploy_rsa.enc -out /tmp/deploy_rsa -d
10 | - eval "$(ssh-agent -s)"
11 | - chmod 600 /tmp/deploy_rsa
12 | - ssh-add /tmp/deploy_rsa
13 |
14 | jobs:
15 | include:
16 | - language: go
17 | go:
18 | - 1.14.x
19 |
20 | before_install:
21 | - cd go
22 | - go get github.com/mattn/goveralls
23 |
24 | before_script:
25 | - psql -c "CREATE USER sound WITH PASSWORD 'sound';" -U postgres
26 | - psql -c "ALTER USER sound WITH SUPERUSER;" -U postgres
27 | - psql -c "CREATE DATABASE sound_test OWNER sound;" -U postgres
28 |
29 | script:
30 | - "$GOPATH/bin/goveralls -service=travis-ci"
31 |
32 | deploy:
33 | provider: script
34 | skip_cleanup: true
35 | script: ./linux_dsm_build.sh &&
36 | ssh admin@hednowley.synology.me -p 169 "if ( sudo status sound | grep start ); then sudo stop sound; fi" &&
37 | scp -P 169 ./sound admin@hednowley.synology.me:/volume1/other/soundDir &&
38 | ssh admin@hednowley.synology.me -p 169 "sudo start sound"
39 |
40 | - language: node_js
41 | node_js:
42 | - 12
43 |
44 | before_install:
45 | - cd elm
46 |
47 | script:
48 | - echo "skipping tests"
49 |
50 | deploy:
51 | provider: script
52 | skip_cleanup: true
53 | script: yarn run build:prod && scp -P 169 ./dist/* admin@hednowley.synology.me:/volume1/other/soundDir/static
54 |
--------------------------------------------------------------------------------
/deploy_rsa.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/deploy_rsa.enc
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | # Database
5 | postgres:
6 | image: postgres:9.3.22
7 | environment:
8 | - POSTGRES_USER=sound
9 | - POSTGRES_PASSWORD=sound
10 | - POSTGRES_DB=sound_test
11 | volumes:
12 | - ./.data/postgres/sound:/var/lib/postgresql/data
13 | ports:
14 | - 5432:5432
15 |
--------------------------------------------------------------------------------
/elm/.gitignore:
--------------------------------------------------------------------------------
1 | # elm-package generated files
2 | elm-stuff
3 | # elm-repl generated files
4 | repl-temp-*
5 | /node_modules
6 | /main.js
7 | /dist/**/*
8 | /dist/*
9 | !/dist/config.js
10 |
--------------------------------------------------------------------------------
/elm/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elm
2 |
3 | elm:
4 | - elm0.19.0
5 |
6 | script:
7 | - elm-format --validate .
8 |
--------------------------------------------------------------------------------
/elm/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:8000",
12 | "webRoot": "${workspaceFolder}",
13 | "runtimeArgs": ["--disable-web-security"], // Allow CORS
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/elm/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "elm.compiler": "./node_modules/.bin/elm",
3 | "elm.makeCommand": "./node_modules/.bin/elm-make",
4 | "elm.formatCommand": "./node_modules/.bin/elm-format",
5 | "[elm]": {
6 | "editor.formatOnSave": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/elm/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Format all elm",
8 | "type": "shell",
9 | "command": "elm-format --yes ."
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/elm/README.md:
--------------------------------------------------------------------------------
1 | # sound ui
2 |
3 | [](https://travis-ci.org/hednowley/sound-ui-elm)
4 |
5 | A work-in-progress front-end for the [sound](https://github.com/hednowley/sound) music server written in [Elm](https://elm-lang.org).
6 |
7 | ## Prerequisites
8 |
9 | You'll need
10 |
11 | - [Node](https://nodejs.org) LTS
12 | - [sound](https://github.com/hednowley/sound)
13 |
14 | ## Building
15 |
16 | ```shell
17 | $ npm install
18 | $ npm run-script build
19 | ```
20 |
21 | The static web app assets will be emitted to `./dist` to be deployed wherever you like.
22 |
--------------------------------------------------------------------------------
/elm/dist/config.js:
--------------------------------------------------------------------------------
1 | var SOUND_CONFIG = {};
2 |
--------------------------------------------------------------------------------
/elm/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/browser": "1.0.2",
10 | "elm/core": "1.0.2",
11 | "elm/html": "1.0.0",
12 | "elm/http": "2.0.0",
13 | "elm/json": "1.1.3",
14 | "elm/random": "1.0.0",
15 | "elm/time": "1.0.0",
16 | "elm/url": "1.0.0",
17 | "elm-community/random-extra": "3.1.0",
18 | "rtfeldman/elm-css": "16.0.1"
19 | },
20 | "indirect": {
21 | "Skinney/murmur3": "2.0.8",
22 | "elm/bytes": "1.0.7",
23 | "elm/file": "1.0.1",
24 | "elm/virtual-dom": "1.0.2",
25 | "owanturist/elm-union-find": "1.0.0",
26 | "rtfeldman/elm-hex": "1.0.0"
27 | }
28 | },
29 | "test-dependencies": {
30 | "direct": {},
31 | "indirect": {}
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/elm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Loading...
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/elm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sound-ui-elm",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "main.js",
6 | "dependencies": {},
7 | "devDependencies": {
8 | "css-loader": "^3.2.1",
9 | "elm": "0.19.1",
10 | "elm-analyse": "^0.16.5",
11 | "elm-format": "^0.8.2",
12 | "elm-review": "^1.0.1",
13 | "elm-webpack-loader": "^6.0.1",
14 | "fibers": "^4.0.2",
15 | "file-loader": "^5.0.2",
16 | "node-sass": "^4.13.0",
17 | "sass": "^1.23.7",
18 | "sass-loader": "^8.0.0",
19 | "style-loader": "^1.0.1",
20 | "webpack": "^4.41.2",
21 | "webpack-cli": "^3.3.10",
22 | "webpack-dev-server": "^3.9.0",
23 | "webpack-merge": "^4.2.2"
24 | },
25 | "scripts": {
26 | "build": "webpack --config webpack.dev.js",
27 | "build:prod": "webpack --config webpack.prod.js",
28 | "start": "webpack-dev-server -d --config webpack.dev.js",
29 | "start:home": "webpack-dev-server -d --config webpack.home.js",
30 | "start:live": "webpack-dev-server -d --config webpack.live.js",
31 | "analyse": "elm-analyse -s"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/hednowley/sound-ui-elm.git"
36 | },
37 | "author": "",
38 | "license": "ISC",
39 | "bugs": {
40 | "url": "https://github.com/hednowley/sound-ui-elm/issues"
41 | },
42 | "homepage": "https://github.com/hednowley/sound-ui-elm#readme"
43 | }
44 |
--------------------------------------------------------------------------------
/elm/review/ReviewConfig.elm:
--------------------------------------------------------------------------------
1 | module ReviewConfig exposing (config)
2 |
3 | {-| Do not rename the ReviewConfig module or the config function, because
4 | `elm-review` will look for these.
5 |
6 | To add packages that contain rules, add them to this review project using
7 |
8 | `elm install author/packagename`
9 |
10 | when inside the directory containing this file.
11 |
12 | -}
13 |
14 | import Review.Rule exposing (Rule)
15 |
16 |
17 | config : List Rule
18 | config =
19 | []
20 |
--------------------------------------------------------------------------------
/elm/review/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "."
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/core": "1.0.2",
10 | "elm/json": "1.1.3",
11 | "jfmengels/elm-review": "1.0.0"
12 | },
13 | "indirect": {
14 | "elm/html": "1.0.0",
15 | "elm/parser": "1.1.0",
16 | "elm/project-metadata-utils": "1.0.0",
17 | "elm/random": "1.0.0",
18 | "elm/time": "1.0.0",
19 | "elm/url": "1.0.0",
20 | "elm/virtual-dom": "1.0.2",
21 | "elm-community/json-extra": "4.2.0",
22 | "elm-community/list-extra": "8.2.2",
23 | "elm-explorations/test": "1.2.2",
24 | "rtfeldman/elm-hex": "1.0.0",
25 | "rtfeldman/elm-iso8601-date-strings": "1.1.3",
26 | "stil4m/elm-syntax": "7.1.1",
27 | "stil4m/structured-writer": "1.0.2"
28 | }
29 | },
30 | "test-dependencies": {
31 | "direct": {},
32 | "indirect": {}
33 | }
34 | }
--------------------------------------------------------------------------------
/elm/src/Album/Fetch.elm:
--------------------------------------------------------------------------------
1 | module Album.Fetch exposing (fetchAlbum)
2 |
3 | import Album.Types exposing (AlbumId, getRawAlbumId)
4 | import Entities.Album exposing (Album)
5 | import Loadable exposing (Loadable(..))
6 | import Model exposing (Model)
7 | import Msg exposing (Msg)
8 | import Nexus.Fetch exposing (fetch)
9 | import Socket.DTO.Album exposing (convert, decode)
10 | import Socket.DTO.SongSummary exposing (convertMany)
11 | import Song.Types exposing (SongId(..), getRawSongId)
12 | import Types exposing (Update)
13 | import Util exposing (insertMany)
14 |
15 |
16 | fetchAlbum : Maybe (Album -> Update Model Msg) -> AlbumId -> Update Model Msg
17 | fetchAlbum maybeCallback =
18 | fetch
19 | getRawAlbumId
20 | "getAlbum"
21 | decode
22 | saveSongs
23 | convert
24 | { get = .albums
25 | , set = \repo -> \m -> { m | albums = repo }
26 | }
27 | maybeCallback
28 |
29 |
30 | saveSongs : Socket.DTO.Album.Album -> Model -> Model
31 | saveSongs album model =
32 | let
33 | songs =
34 | convertMany album.songs
35 | in
36 | { model
37 | | songs =
38 | insertMany
39 | (.id >> getRawSongId)
40 | identity
41 | songs
42 | model.songs
43 | }
44 |
--------------------------------------------------------------------------------
/elm/src/Album/Select.elm:
--------------------------------------------------------------------------------
1 | module Album.Select exposing (getAlbum, getAlbumArt, getAlbumSongs)
2 |
3 | import Album.Types exposing (AlbumId, getRawAlbumId)
4 | import Dict
5 | import Entities.Album exposing (Album)
6 | import Entities.SongSummary exposing (SongSummary)
7 | import Loadable exposing (Loadable(..))
8 | import Model exposing (Model)
9 | import Song.Select exposing (getSong)
10 |
11 |
12 | getAlbum : AlbumId -> Model -> Loadable Album
13 | getAlbum albumId model =
14 | Dict.get (getRawAlbumId albumId) model.nexus.albums |> Maybe.withDefault Absent
15 |
16 |
17 | getAlbumSongs : Album -> Model -> List (Maybe SongSummary)
18 | getAlbumSongs album model =
19 | List.map (getSong model) album.songs
20 |
21 |
22 |
23 | -- |> List.sortBy .track
24 |
25 |
26 | getAlbumArt : Maybe String -> String
27 | getAlbumArt art =
28 | case art of
29 | Nothing ->
30 | ""
31 |
32 | Just id ->
33 | "/api/art?size=120&id=" ++ id
34 |
--------------------------------------------------------------------------------
/elm/src/Album/Types.elm:
--------------------------------------------------------------------------------
1 | module Album.Types exposing (AlbumId(..), getRawAlbumId)
2 |
3 |
4 | type AlbumId
5 | = AlbumId Int
6 |
7 |
8 | getRawAlbumId : AlbumId -> Int
9 | getRawAlbumId albumId =
10 | let
11 | (AlbumId raw) =
12 | albumId
13 | in
14 | raw
15 |
--------------------------------------------------------------------------------
/elm/src/Album/Update.elm:
--------------------------------------------------------------------------------
1 | module Album.Update exposing (playAlbum)
2 |
3 | import Album.Fetch exposing (fetchAlbum)
4 | import Album.Types exposing (AlbumId)
5 | import Audio.Msg exposing (AudioMsg(..))
6 | import Entities.Album exposing (Album)
7 | import Loadable exposing (Loadable(..))
8 | import Model exposing (Model)
9 | import Msg exposing (Msg(..))
10 | import Player.Actions exposing (replacePlaylist)
11 | import Types exposing (Update)
12 |
13 |
14 | playLoadedAlbum : Album -> Update Model Msg
15 | playLoadedAlbum album =
16 | replacePlaylist album.songs
17 |
18 |
19 | playAlbum : AlbumId -> Update Model Msg
20 | playAlbum albumId =
21 | fetchAlbum
22 | (Just playLoadedAlbum)
23 | albumId
24 |
--------------------------------------------------------------------------------
/elm/src/Artist/Fetch.elm:
--------------------------------------------------------------------------------
1 | module Artist.Fetch exposing (fetchArtist)
2 |
3 | import Artist.Types exposing (ArtistId, getRawArtistId)
4 | import Entities.Artist exposing (Artist)
5 | import Loadable exposing (Loadable(..))
6 | import Model exposing (Model)
7 | import Msg exposing (Msg)
8 | import Nexus.Fetch exposing (fetch)
9 | import Socket.DTO.Artist exposing (convert, decode)
10 | import Song.Types exposing (SongId(..))
11 | import Types exposing (Update)
12 |
13 |
14 | noOp : Socket.DTO.Artist.Artist -> Model -> Model
15 | noOp _ model =
16 | model
17 |
18 |
19 | fetchArtist : Maybe (Artist -> Update Model Msg) -> ArtistId -> Update Model Msg
20 | fetchArtist maybeCallback =
21 | fetch
22 | getRawArtistId
23 | "getArtist"
24 | decode
25 | noOp
26 | convert
27 | { get = .artists
28 | , set = \repo -> \m -> { m | artists = repo }
29 | }
30 | maybeCallback
31 |
--------------------------------------------------------------------------------
/elm/src/Artist/Select.elm:
--------------------------------------------------------------------------------
1 | module Artist.Select exposing (getArtist)
2 |
3 | import Artist.Types exposing (ArtistId, getRawArtistId)
4 | import Dict
5 | import Entities.Artist exposing (Artist)
6 | import Loadable exposing (Loadable(..))
7 | import Model exposing (Model)
8 |
9 |
10 | getArtist : ArtistId -> Model -> Loadable Artist
11 | getArtist id model =
12 | Dict.get (getRawArtistId id) model.nexus.artists |> Maybe.withDefault Absent
13 |
--------------------------------------------------------------------------------
/elm/src/Artist/Types.elm:
--------------------------------------------------------------------------------
1 | module Artist.Types exposing (ArtistId(..), getRawArtistId)
2 |
3 |
4 | type ArtistId
5 | = ArtistId Int
6 |
7 |
8 | getRawArtistId : ArtistId -> Int
9 | getRawArtistId albumId =
10 | let
11 | (ArtistId raw) =
12 | albumId
13 | in
14 | raw
15 |
--------------------------------------------------------------------------------
/elm/src/Audio/Model.elm:
--------------------------------------------------------------------------------
1 | module Audio.Model exposing (Model, emptyModel)
2 |
3 | import Audio.State exposing (State)
4 | import Dict exposing (Dict)
5 |
6 |
7 | emptyModel : Model
8 | emptyModel =
9 | { songs = Dict.empty
10 | }
11 |
12 |
13 | type alias Model =
14 | { songs : Dict Int State
15 | }
16 |
--------------------------------------------------------------------------------
/elm/src/Audio/Msg.elm:
--------------------------------------------------------------------------------
1 | module Audio.Msg exposing (AudioMsg(..))
2 |
3 | import Song.Types exposing (SongId)
4 |
5 |
6 | type AudioMsg
7 | = CanPlay SongId -- A song is ready to be played
8 | | Ended SongId
9 | | SetTime Float
10 | | TimeChanged { songId : Int, time : Float }
11 | | Playing { songId : Int, time : Float, duration : Maybe Float }
12 | | Paused { songId : Int, time : Float, duration : Maybe Float }
13 |
--------------------------------------------------------------------------------
/elm/src/Audio/Request.elm:
--------------------------------------------------------------------------------
1 | module Audio.Request exposing (LoadRequest, makeLoadRequest)
2 |
3 | import Song.Types exposing (SongId, getRawSongId)
4 | import String exposing (fromInt)
5 |
6 |
7 | type alias LoadRequest =
8 | { url : String
9 | , songId : Int
10 | }
11 |
12 |
13 | makeLoadRequest : SongId -> LoadRequest
14 | makeLoadRequest songId =
15 | { url = "/api/stream?id=" ++ fromInt (getRawSongId songId)
16 | , songId = getRawSongId songId
17 | }
18 |
--------------------------------------------------------------------------------
/elm/src/Audio/Select.elm:
--------------------------------------------------------------------------------
1 | module Audio.Select exposing (getSongState)
2 |
3 | import Audio.State exposing (State(..))
4 | import Dict
5 | import Loadable exposing (Loadable(..))
6 | import Model exposing (Model)
7 | import Routing exposing (Route(..))
8 | import Song.Types exposing (SongId(..), getRawSongId)
9 |
10 |
11 | getSongState : Model -> SongId -> Maybe State
12 | getSongState model songId =
13 | Dict.get (getRawSongId songId) model.audio.songs
14 |
--------------------------------------------------------------------------------
/elm/src/Audio/State.elm:
--------------------------------------------------------------------------------
1 | module Audio.State exposing (State(..))
2 |
3 |
4 | type State
5 | = Loading
6 | | Loaded { duration : Maybe Float }
7 | | Playing { time : Float, duration : Maybe Float, paused : Bool }
8 |
--------------------------------------------------------------------------------
/elm/src/Audio/Update.elm:
--------------------------------------------------------------------------------
1 | module Audio.Update exposing (update)
2 |
3 | import Audio.Actions exposing (onSongLoaded, onTimeChanged, updateSongState)
4 | import Audio.Msg exposing (AudioMsg(..))
5 | import Audio.State
6 | import Model exposing (Model)
7 | import Msg exposing (Msg)
8 | import Player.Actions
9 | exposing
10 | ( onSongEnded
11 | , setCurrentTime
12 | )
13 | import Song.Types exposing (SongId(..))
14 | import Types exposing (Update)
15 |
16 |
17 | update : AudioMsg -> Update Model Msg
18 | update msg model =
19 | case msg of
20 | CanPlay songId ->
21 | onSongLoaded songId model
22 |
23 | Ended _ ->
24 | onSongEnded model
25 |
26 | SetTime time ->
27 | setCurrentTime time model
28 |
29 | Playing { songId, time, duration } ->
30 | ( updateSongState
31 | (SongId songId)
32 | (Audio.State.Playing { paused = False, time = time, duration = duration })
33 | model
34 | , Cmd.none
35 | )
36 |
37 | Paused { songId, time, duration } ->
38 | ( updateSongState
39 | (SongId songId)
40 | (Audio.State.Playing { paused = True, time = time, duration = duration })
41 | model
42 | , Cmd.none
43 | )
44 |
45 | TimeChanged args ->
46 | ( onTimeChanged
47 | (SongId args.songId)
48 | args.time
49 | model
50 | , Cmd.none
51 | )
52 |
--------------------------------------------------------------------------------
/elm/src/Cache.elm:
--------------------------------------------------------------------------------
1 | module Cache exposing (Cache, makeCache, makeModel, tryDecode)
2 |
3 | import Json.Decode as Decode exposing (Decoder)
4 | import Loadable exposing (fromMaybe, toMaybe)
5 | import Model exposing (Model)
6 |
7 |
8 | {-| A version of the model which can be stored in the browser.
9 | -}
10 | type alias Cache =
11 | { token : Maybe String }
12 |
13 |
14 | {-| Create a cache from a model.
15 | -}
16 | makeCache : Model -> Cache
17 | makeCache model =
18 | { token = toMaybe model.token }
19 |
20 |
21 | {-| Create a model from a cache.
22 | -}
23 | makeModel : Model -> Maybe Cache -> Model
24 | makeModel default cache =
25 | case cache of
26 | Just c ->
27 | { default | token = fromMaybe c.token }
28 |
29 | Nothing ->
30 | default
31 |
32 |
33 | {-| Try to decode a cache from an optional JSON value.
34 | -}
35 | tryDecode : Maybe Decode.Value -> Maybe Cache
36 | tryDecode =
37 | Maybe.andThen <| Decode.decodeValue decode >> Result.toMaybe
38 |
39 |
40 | decode : Decoder Cache
41 | decode =
42 | Decode.map Cache
43 | (Decode.field "token" (Decode.maybe Decode.string))
44 |
--------------------------------------------------------------------------------
/elm/src/Config.elm:
--------------------------------------------------------------------------------
1 | module Config exposing (Config)
2 |
3 |
4 | type alias Config =
5 | {}
6 |
--------------------------------------------------------------------------------
/elm/src/DTO/Authenticate.elm:
--------------------------------------------------------------------------------
1 | module DTO.Authenticate exposing (Response, decode)
2 |
3 | import Json.Decode exposing (Decoder, andThen, fail, field, map, string, succeed)
4 |
5 |
6 | {-| A potential error message. Absense means success.
7 | -}
8 | type alias Response =
9 | Maybe String
10 |
11 |
12 | {-| Decode the message.
13 | -}
14 | decode : Decoder Response
15 | decode =
16 | field "status" string
17 | |> andThen decodeData
18 |
19 |
20 | {-| Decode the data portion of the message.
21 | -}
22 | decodeData : String -> Decoder Response
23 | decodeData status =
24 | case status of
25 | "success" ->
26 | succeed Nothing
27 |
28 | "error" ->
29 | map Just (field "data" string)
30 |
31 | _ ->
32 | fail "Unknown status"
33 |
--------------------------------------------------------------------------------
/elm/src/DTO/Credentials.elm:
--------------------------------------------------------------------------------
1 | module DTO.Credentials exposing (credentialsEncoder)
2 |
3 | import Json.Encode
4 |
5 |
6 | credentialsEncoder : String -> String -> Json.Encode.Value
7 | credentialsEncoder username password =
8 | Json.Encode.object
9 | [ ( "username", Json.Encode.string username )
10 | , ( "password", Json.Encode.string password )
11 | ]
12 |
--------------------------------------------------------------------------------
/elm/src/DTO/Ticket.elm:
--------------------------------------------------------------------------------
1 | module DTO.Ticket exposing (decode)
2 |
3 | import Json.Decode exposing (Decoder, field, string)
4 |
5 |
6 | decode : Decoder String
7 | decode =
8 | field "data" (field "ticket" string)
9 |
--------------------------------------------------------------------------------
/elm/src/Entities/Album.elm:
--------------------------------------------------------------------------------
1 | module Entities.Album exposing (Album)
2 |
3 | import Song.Types exposing (SongId)
4 |
5 |
6 | type alias Album =
7 | { id : Int
8 | , artId : Maybe String
9 | , name : String
10 | , songs : List SongId
11 | }
12 |
--------------------------------------------------------------------------------
/elm/src/Entities/AlbumSummary.elm:
--------------------------------------------------------------------------------
1 | module Entities.AlbumSummary exposing (AlbumSummaries, AlbumSummary)
2 |
3 | import Album.Types exposing (AlbumId)
4 | import Dict exposing (Dict)
5 |
6 |
7 | type alias AlbumSummary =
8 | { id : AlbumId
9 | , name : String
10 | , duration : Int
11 | , year : Maybe Int
12 | , artId : Maybe String
13 | }
14 |
15 |
16 | type alias AlbumSummaries =
17 | Dict Int AlbumSummary
18 |
--------------------------------------------------------------------------------
/elm/src/Entities/Artist.elm:
--------------------------------------------------------------------------------
1 | module Entities.Artist exposing (Artist)
2 |
3 | import Artist.Types exposing (ArtistId)
4 | import Entities.AlbumSummary exposing (AlbumSummary)
5 |
6 |
7 | type alias Artist =
8 | { id : ArtistId
9 | , name : String
10 | , albums : List AlbumSummary
11 | }
12 |
--------------------------------------------------------------------------------
/elm/src/Entities/ArtistSummary.elm:
--------------------------------------------------------------------------------
1 | module Entities.ArtistSummary exposing (ArtistSummaries, ArtistSummary)
2 |
3 | import Artist.Types exposing (ArtistId)
4 | import Dict exposing (Dict)
5 |
6 |
7 | type alias ArtistSummary =
8 | { id : ArtistId
9 | , name : String
10 | }
11 |
12 |
13 | type alias ArtistSummaries =
14 | Dict Int ArtistSummary
15 |
--------------------------------------------------------------------------------
/elm/src/Entities/Playlist.elm:
--------------------------------------------------------------------------------
1 | module Entities.Playlist exposing (Playlist)
2 |
3 | import Song.Types exposing (SongId)
4 |
5 |
6 | type alias Playlist =
7 | { id : Int
8 | , name : String
9 | , songs : List SongId
10 | }
11 |
--------------------------------------------------------------------------------
/elm/src/Entities/PlaylistSummary.elm:
--------------------------------------------------------------------------------
1 | module Entities.PlaylistSummary exposing (PlaylistSummaries, PlaylistSummary)
2 |
3 | import Dict exposing (Dict)
4 |
5 |
6 | type alias PlaylistSummary =
7 | { id : Int
8 | , name : String
9 | }
10 |
11 |
12 | type alias PlaylistSummaries =
13 | Dict Int PlaylistSummary
14 |
--------------------------------------------------------------------------------
/elm/src/Entities/SongSummary.elm:
--------------------------------------------------------------------------------
1 | module Entities.SongSummary exposing (SongSummary)
2 |
3 | import Song.Types exposing (SongId)
4 |
5 |
6 | type alias SongSummary =
7 | { id : SongId
8 | , name : String
9 | , track : Int
10 | }
11 |
--------------------------------------------------------------------------------
/elm/src/Loadable.elm:
--------------------------------------------------------------------------------
1 | module Loadable exposing (Loadable(..), fromMaybe, toMaybe)
2 |
3 | import Socket.MessageId exposing (MessageId)
4 |
5 |
6 | {-| Something which takes time to load.
7 | -}
8 | type Loadable value
9 | = Absent
10 | | Loading MessageId
11 | | Loaded value
12 |
13 |
14 | {-| Convert a Loadable to a Maybe.
15 | -}
16 | toMaybe : Loadable value -> Maybe value
17 | toMaybe loadable =
18 | case loadable of
19 | Loaded v ->
20 | Just v
21 |
22 | _ ->
23 | Nothing
24 |
25 |
26 | {-| Convert a Maybe into a Loadable.
27 | -}
28 | fromMaybe : Maybe value -> Loadable value
29 | fromMaybe maybe =
30 | case maybe of
31 | Just v ->
32 | Loaded v
33 |
34 | _ ->
35 | Absent
36 |
--------------------------------------------------------------------------------
/elm/src/Model.elm:
--------------------------------------------------------------------------------
1 | module Model exposing (Model, SocketModelWrap(..), getSocketModel, setSocketModel)
2 |
3 | import Audio.Model
4 | import Browser.Navigation exposing (Key)
5 | import Config exposing (Config)
6 | import Dict exposing (Dict)
7 | import Entities.AlbumSummary exposing (AlbumSummaries)
8 | import Entities.ArtistSummary exposing (ArtistSummaries)
9 | import Entities.PlaylistSummary exposing (PlaylistSummaries)
10 | import Entities.SongSummary exposing (SongSummary)
11 | import Loadable exposing (Loadable(..))
12 | import Nexus.Model
13 | import Player.Model
14 | import Routing exposing (Route)
15 | import Socket.Model
16 | import Url exposing (Url)
17 |
18 |
19 | type alias Model =
20 | { key : Key
21 | , url : Url
22 | , username : String
23 | , password : String
24 | , message : String
25 | , token : Loadable String
26 | , isScanning : Bool
27 | , scanCount : Int
28 | , scanShouldUpdate : Bool
29 | , scanShouldDelete : Bool
30 | , playlists : PlaylistSummaries
31 | , nexus : Nexus.Model.Model
32 | , artists : ArtistSummaries
33 | , albums : AlbumSummaries
34 | , songs : Dict Int SongSummary
35 | , config : Config
36 | , route : Maybe Route
37 | , socket : SocketModelWrap
38 | , audio : Audio.Model.Model
39 | , player : Player.Model.Model
40 | }
41 |
42 |
43 | {-| Type to avoid type recursion
44 | -}
45 | type SocketModelWrap
46 | = SocketModelWrap (Socket.Model.Model Model)
47 |
48 |
49 | getSocketModel : Model -> Socket.Model.Model Model
50 | getSocketModel model =
51 | let
52 | (SocketModelWrap s) =
53 | model.socket
54 | in
55 | s
56 |
57 |
58 | setSocketModel : Model -> Socket.Model.Model Model -> Model
59 | setSocketModel model socket =
60 | { model
61 | | socket = SocketModelWrap socket
62 | }
63 |
--------------------------------------------------------------------------------
/elm/src/Msg.elm:
--------------------------------------------------------------------------------
1 | module Msg exposing (Msg(..))
2 |
3 | import Audio.Msg exposing (AudioMsg)
4 | import Browser
5 | import DTO.Authenticate
6 | import Http
7 | import Player.Msg exposing (PlayerMsg)
8 | import Socket.SocketMsg exposing (SocketMsg)
9 | import Url
10 |
11 |
12 | type Msg
13 | = OnUrlChange Url.Url
14 | | OnUrlRequest Browser.UrlRequest
15 | | UsernameChanged String
16 | | PasswordChanged String
17 | | SubmitLogin
18 | | LogOut
19 | | ToggleScanUpdate
20 | | ToggleScanDelete
21 | | StartScan -- Ask for a scan to be started
22 | | GotAuthenticateResponse (Result Http.Error DTO.Authenticate.Response) -- Server has replied to posting of credentials
23 | | GotTicketResponse (Result Http.Error String) -- Server has replied to a request for a websocket ticket
24 | | SocketMsg SocketMsg
25 | | PlayerMsg PlayerMsg
26 | | AudioMsg AudioMsg
27 |
--------------------------------------------------------------------------------
/elm/src/Nexus/Callback.elm:
--------------------------------------------------------------------------------
1 | module Nexus.Callback exposing (Callback, resolve)
2 |
3 | import Loadable exposing (Loadable(..))
4 | import Model exposing (Model)
5 | import Msg exposing (Msg)
6 | import Types exposing (Update)
7 |
8 |
9 | type alias Callback a =
10 | a -> Update Model Msg
11 |
12 |
13 | resolve : Maybe (Callback a) -> Callback a
14 | resolve maybeCallback =
15 | Maybe.withDefault
16 | (\a -> \m -> ( m, Cmd.none ))
17 | maybeCallback
18 |
--------------------------------------------------------------------------------
/elm/src/Nexus/Model.elm:
--------------------------------------------------------------------------------
1 | module Nexus.Model exposing (Model, empty)
2 |
3 | import Dict exposing (Dict)
4 | import Entities.Album exposing (Album)
5 | import Entities.Artist exposing (Artist)
6 | import Entities.Playlist exposing (Playlist)
7 | import Loadable exposing (Loadable(..))
8 |
9 |
10 | type alias Model =
11 | { playlists : Dict Int (Loadable Playlist)
12 | , artists : Dict Int (Loadable Artist)
13 | , albums : Dict Int (Loadable Album)
14 | }
15 |
16 |
17 | empty : Model
18 | empty =
19 | { playlists = Dict.empty
20 | , artists = Dict.empty
21 | , albums = Dict.empty
22 | }
23 |
--------------------------------------------------------------------------------
/elm/src/Player/Model.elm:
--------------------------------------------------------------------------------
1 | module Player.Model exposing (Model, emptyModel)
2 |
3 | import Array exposing (Array)
4 | import Player.Repeat exposing (Repeat(..))
5 | import Song.Types exposing (SongId)
6 |
7 |
8 | emptyModel : Model
9 | emptyModel =
10 | { shuffle = False
11 | , repeat = None
12 | , playlist = Array.empty
13 | , unshuffledPlaylist = Array.empty
14 | , playing = Nothing
15 | , isPaused = True
16 | }
17 |
18 |
19 | type alias Model =
20 | { shuffle : Bool
21 | , repeat : Repeat
22 | , playlist : Array SongId
23 | , unshuffledPlaylist : Array SongId
24 | , playing : Maybe Int
25 | , isPaused : Bool
26 | }
27 |
--------------------------------------------------------------------------------
/elm/src/Player/Msg.elm:
--------------------------------------------------------------------------------
1 | module Player.Msg exposing (PlayerMsg(..))
2 |
3 | import Album.Types exposing (AlbumId)
4 | import Array exposing (Array)
5 | import Player.Repeat exposing (Repeat)
6 | import Playlist.Types exposing (PlaylistId)
7 | import Song.Types exposing (SongId)
8 |
9 |
10 | type PlayerMsg
11 | = Play SongId
12 | | PlayItem Int
13 | | Pause
14 | | Resume
15 | | Queue SongId
16 | | PlayAlbum AlbumId
17 | | PlayPlaylist PlaylistId
18 | | Next
19 | | Prev
20 | | SetShuffle Bool
21 | | Shuffled (Array SongId)
22 | | SetRepeat Repeat
23 |
--------------------------------------------------------------------------------
/elm/src/Player/Repeat.elm:
--------------------------------------------------------------------------------
1 | module Player.Repeat exposing (Repeat(..))
2 |
3 |
4 | type Repeat
5 | = None
6 | | One
7 | | All
8 |
--------------------------------------------------------------------------------
/elm/src/Player/Select.elm:
--------------------------------------------------------------------------------
1 | module Player.Select exposing (getCurrentSongId, getCurrentSongState, getSongId, isPlaying, shuffleIsOn)
2 |
3 | import Array
4 | import Audio.Select exposing (getSongState)
5 | import Audio.State exposing (State(..))
6 | import Loadable exposing (Loadable(..))
7 | import Model exposing (Model)
8 | import Routing exposing (Route(..))
9 | import Song.Types exposing (SongId(..))
10 |
11 |
12 | {-| Gets the ID of the song at the given position in the playlist.
13 | -}
14 | getSongId : Model -> Int -> Maybe SongId
15 | getSongId model index =
16 | Array.get index model.player.playlist
17 |
18 |
19 | {-| Gets the ID of the currently playing song.
20 | -}
21 | getCurrentSongId : Model -> Maybe SongId
22 | getCurrentSongId model =
23 | model.player.playing
24 | |> Maybe.andThen (getSongId model)
25 |
26 |
27 | getCurrentSongState : Model -> Maybe State
28 | getCurrentSongState model =
29 | getCurrentSongId model |> Maybe.andThen (getSongState model)
30 |
31 |
32 | shuffleIsOn : Model -> Bool
33 | shuffleIsOn model =
34 | model.player.shuffle
35 |
36 |
37 | isPlaying : Model -> Bool
38 | isPlaying model =
39 | case getCurrentSongState model of
40 | Just (Playing _) ->
41 | True
42 |
43 | _ ->
44 | False
45 |
--------------------------------------------------------------------------------
/elm/src/Player/Update.elm:
--------------------------------------------------------------------------------
1 | module Player.Update exposing (update)
2 |
3 | import Album.Update exposing (playAlbum)
4 | import Audio.Msg exposing (AudioMsg(..))
5 | import Model exposing (Model)
6 | import Msg
7 | import Player.Actions
8 | exposing
9 | ( finishShufflePlaylist
10 | , goNext
11 | , goPrev
12 | , pauseCurrent
13 | , playItem
14 | , queueAndPlaySong
15 | , queueSong
16 | , resumeCurrent
17 | , setRepeat
18 | , setShuffle
19 | )
20 | import Player.Msg exposing (PlayerMsg(..))
21 | import Playlist.Update exposing (playPlaylist)
22 | import Song.Types exposing (SongId(..))
23 | import Types exposing (Update)
24 |
25 |
26 | update : PlayerMsg -> Update Model Msg.Msg
27 | update msg model =
28 | case msg of
29 | PlayItem index ->
30 | playItem index model
31 |
32 | Play songId ->
33 | queueAndPlaySong songId model
34 |
35 | PlayAlbum albumId ->
36 | playAlbum albumId model
37 |
38 | PlayPlaylist playlistId ->
39 | playPlaylist playlistId model
40 |
41 | Pause ->
42 | pauseCurrent model
43 |
44 | Resume ->
45 | resumeCurrent model
46 |
47 | Queue songId ->
48 | ( queueSong songId model, Cmd.none )
49 |
50 | Next ->
51 | goNext model
52 |
53 | Prev ->
54 | goPrev model
55 |
56 | SetShuffle on ->
57 | setShuffle on model
58 |
59 | Shuffled playlist ->
60 | ( finishShufflePlaylist playlist model, Cmd.none )
61 |
62 | SetRepeat repeat ->
63 | setRepeat repeat model
64 |
--------------------------------------------------------------------------------
/elm/src/Playlist/Fetch.elm:
--------------------------------------------------------------------------------
1 | module Playlist.Fetch exposing (fetchPlaylist)
2 |
3 | import Entities.Playlist exposing (Playlist)
4 | import Loadable exposing (Loadable(..))
5 | import Model exposing (Model)
6 | import Msg exposing (Msg)
7 | import Nexus.Fetch exposing (fetch)
8 | import Playlist.Types exposing (PlaylistId, getRawPlaylistId)
9 | import Socket.DTO.Playlist exposing (convert, decode)
10 | import Socket.DTO.SongSummary exposing (convertMany)
11 | import Song.Types exposing (SongId(..), getRawSongId)
12 | import Types exposing (Update)
13 | import Util exposing (insertMany)
14 |
15 |
16 | fetchPlaylist : Maybe (Playlist -> Update Model Msg) -> PlaylistId -> Update Model Msg
17 | fetchPlaylist maybeCallback =
18 | fetch
19 | getRawPlaylistId
20 | "getPlaylist"
21 | decode
22 | saveSongs
23 | convert
24 | { get = .playlists
25 | , set = \repo -> \m -> { m | playlists = repo }
26 | }
27 | maybeCallback
28 |
29 |
30 | saveSongs : Socket.DTO.Playlist.Playlist -> Model -> Model
31 | saveSongs playlist model =
32 | let
33 | songs =
34 | convertMany playlist.songs
35 | in
36 | { model
37 | | songs =
38 | insertMany
39 | (.id >> getRawSongId)
40 | identity
41 | songs
42 | model.songs
43 | }
44 |
--------------------------------------------------------------------------------
/elm/src/Playlist/Select.elm:
--------------------------------------------------------------------------------
1 | module Playlist.Select exposing (getPlaylist, getPlaylistSongs)
2 |
3 | import Dict
4 | import Entities.Playlist exposing (Playlist)
5 | import Entities.SongSummary exposing (SongSummary)
6 | import Loadable exposing (Loadable(..))
7 | import Model exposing (Model)
8 | import Playlist.Types exposing (PlaylistId, getRawPlaylistId)
9 | import Song.Select exposing (getSong)
10 |
11 |
12 | getPlaylist : PlaylistId -> Model -> Loadable Playlist
13 | getPlaylist id model =
14 | Dict.get (getRawPlaylistId id) model.nexus.playlists |> Maybe.withDefault Absent
15 |
16 |
17 | getPlaylistSongs : Playlist -> Model -> List (Maybe SongSummary)
18 | getPlaylistSongs playlist model =
19 | List.map (getSong model) playlist.songs
20 |
--------------------------------------------------------------------------------
/elm/src/Playlist/Types.elm:
--------------------------------------------------------------------------------
1 | module Playlist.Types exposing (PlaylistId(..), getRawPlaylistId)
2 |
3 |
4 | type PlaylistId
5 | = PlaylistId Int
6 |
7 |
8 | getRawPlaylistId : PlaylistId -> Int
9 | getRawPlaylistId playlistId =
10 | let
11 | (PlaylistId raw) =
12 | playlistId
13 | in
14 | raw
15 |
--------------------------------------------------------------------------------
/elm/src/Playlist/Update.elm:
--------------------------------------------------------------------------------
1 | module Playlist.Update exposing (playPlaylist)
2 |
3 | import Audio.Msg exposing (AudioMsg(..))
4 | import Entities.Playlist exposing (Playlist)
5 | import Loadable exposing (Loadable(..))
6 | import Model exposing (Model)
7 | import Msg exposing (Msg(..))
8 | import Player.Actions exposing (replacePlaylist)
9 | import Playlist.Fetch exposing (fetchPlaylist)
10 | import Playlist.Types exposing (PlaylistId)
11 | import Types exposing (Update)
12 |
13 |
14 | playLoadedPlaylist : Playlist -> Update Model Msg
15 | playLoadedPlaylist playlist =
16 | replacePlaylist playlist.songs
17 |
18 |
19 | playPlaylist : PlaylistId -> Update Model Msg
20 | playPlaylist playlistId =
21 | fetchPlaylist
22 | (Just playLoadedPlaylist)
23 | playlistId
24 |
--------------------------------------------------------------------------------
/elm/src/Ports.elm:
--------------------------------------------------------------------------------
1 | port module Ports exposing (..)
2 |
3 | import Audio.Request
4 | import Cache exposing (Cache)
5 | import Json.Encode
6 |
7 |
8 |
9 | -- Outgoing ports
10 |
11 |
12 | port setCache : Cache -> Cmd msg
13 |
14 |
15 | port loadAudio : Audio.Request.LoadRequest -> Cmd msg
16 |
17 |
18 | port playAudio : Int -> Cmd msg
19 |
20 |
21 | port pauseAudio : Int -> Cmd msg
22 |
23 |
24 | port resumeAudio : Int -> Cmd msg
25 |
26 |
27 | port setAudioTime : { songId : Int, time : Float } -> Cmd msg
28 |
29 |
30 | port websocketOut : Json.Encode.Value -> Cmd msg
31 |
32 |
33 | port websocketOpen : String -> Cmd msg
34 |
35 |
36 | port websocketClose : () -> Cmd msg
37 |
38 |
39 |
40 | -- Incoming ports
41 |
42 |
43 | port canPlayAudio : (Int -> msg) -> Sub msg
44 |
45 |
46 | port audioEnded : (Int -> msg) -> Sub msg
47 |
48 |
49 | port audioPlaying : ({ songId : Int, time : Float, duration : Maybe Float } -> msg) -> Sub msg
50 |
51 |
52 | port audioPaused : ({ songId : Int, time : Float, duration : Maybe Float } -> msg) -> Sub msg
53 |
54 |
55 | port audioTimeChanged : ({ songId : Int, time : Float } -> msg) -> Sub msg
56 |
57 |
58 | port audioNextPressed : (() -> msg) -> Sub msg
59 |
60 |
61 | port audioPrevPressed : (() -> msg) -> Sub msg
62 |
63 |
64 | port websocketOpened : (() -> msg) -> Sub msg
65 |
66 |
67 | port websocketClosed : (() -> msg) -> Sub msg
68 |
69 |
70 | port websocketIn : (String -> msg) -> Sub msg
71 |
--------------------------------------------------------------------------------
/elm/src/Routing.elm:
--------------------------------------------------------------------------------
1 | module Routing exposing (Route(..), getWebsocketUrl, parseUrl)
2 |
3 | import Album.Types exposing (AlbumId(..))
4 | import Artist.Types exposing (ArtistId(..))
5 | import Playlist.Types exposing (PlaylistId(..))
6 | import String exposing (fromInt)
7 | import Url exposing (Url)
8 | import Url.Parser exposing ((>), Parser, int, map, oneOf, parse, s)
9 |
10 |
11 | getWebsocketUrl : Url -> String
12 | getWebsocketUrl url =
13 | let
14 | port_ =
15 | case url.port_ of
16 | Just p ->
17 | ":" ++ fromInt p
18 |
19 | Nothing ->
20 | ""
21 | in
22 | "ws://" ++ url.host ++ port_ ++ "/ws"
23 |
24 |
25 | type Route
26 | = Artist ArtistId
27 | | Album AlbumId
28 | | Playlist PlaylistId
29 | | Artists
30 | | Albums
31 | | Playlists
32 |
33 |
34 | parseUrl : Url -> Maybe Route
35 | parseUrl =
36 | parse routeParser
37 |
38 |
39 | routeParser : Parser (Route -> a) a
40 | routeParser =
41 | oneOf
42 | [ map (ArtistId >> Artist) (s "artist" > int)
43 | , map (AlbumId >> Album) (s "album" > int)
44 | , map (PlaylistId >> Playlist) (s "playlist" > int)
45 | , map Artists (s "artists")
46 | , map Albums (s "albums")
47 | , map Playlists (s "playlists")
48 | ]
49 |
--------------------------------------------------------------------------------
/elm/src/Socket/Actions.elm:
--------------------------------------------------------------------------------
1 | module Socket.Actions exposing (addListener, addListenerExternal, removeListener)
2 |
3 | import Dict
4 | import Model exposing (getSocketModel, setSocketModel)
5 | import Msg exposing (Msg)
6 | import Socket.Listener exposing (Listener, combineListeners)
7 | import Socket.MessageId exposing (MessageId, getRawMessageId)
8 | import Socket.Model exposing (Model)
9 | import Socket.Select exposing (getListener)
10 |
11 |
12 | type alias Model =
13 | Socket.Model.Model Model.Model
14 |
15 |
16 | addListenerExternal : MessageId -> Listener Model.Model Msg -> Model.Model -> Model.Model
17 | addListenerExternal id listener model =
18 | let
19 | socket =
20 | getSocketModel model
21 | in
22 | addListener id listener socket |> setSocketModel model
23 |
24 |
25 | {-| Store a new Websocket listener in the model.
26 | -}
27 | addListener : MessageId -> Listener Model.Model Msg -> Model -> Model
28 | addListener id listener model =
29 | case getListener id model of
30 | Just existing ->
31 | let
32 | combined =
33 | combineListeners existing listener
34 | in
35 | insertListener id combined model
36 |
37 | Nothing ->
38 | insertListener id listener model
39 |
40 |
41 | insertListener : MessageId -> Listener Model.Model Msg -> Model -> Model
42 | insertListener messageId listener model =
43 | { model
44 | | listeners =
45 | Dict.insert (getRawMessageId messageId) listener model.listeners
46 | }
47 |
48 |
49 | {-| Remove a stored Websocket listener from the model.
50 | -}
51 | removeListener : Int -> Model -> Model
52 | removeListener id model =
53 | { model
54 | | listeners =
55 | Dict.remove id model.listeners
56 | }
57 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/Album.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.Album exposing (Album, convert, decode)
2 |
3 | import Entities.Album
4 | import Json.Decode exposing (Decoder, field, int, list, map6, maybe, string)
5 | import Socket.DTO.SongSummary exposing (SongSummary)
6 | import Song.Types exposing (SongId(..))
7 |
8 |
9 | type alias Album =
10 | { id : Int
11 | , artId : Maybe String
12 | , name : String
13 | , duration : Int
14 | , year : Maybe Int
15 | , songs : List SongSummary
16 | }
17 |
18 |
19 | decode : Decoder Album
20 | decode =
21 | map6 Album
22 | (field "id" int)
23 | (maybe <| field "coverArt" string)
24 | (field "name" string)
25 | (field "duration" int)
26 | (maybe <| field "year" int)
27 | (field "songs" <| list Socket.DTO.SongSummary.decode)
28 |
29 |
30 | convert : Album -> Entities.Album.Album
31 | convert album =
32 | { id = album.id
33 | , artId = album.artId
34 | , name = album.name
35 | , songs = List.map (.id >> SongId) album.songs
36 | }
37 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/AlbumSummary.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.AlbumSummary exposing (AlbumSummary, convert, decode)
2 |
3 | import Album.Types exposing (AlbumId(..))
4 | import Entities.AlbumSummary
5 | import Json.Decode exposing (Decoder, field, int, map5, maybe, string)
6 |
7 |
8 | type alias AlbumSummary =
9 | { id : Int
10 | , name : String
11 | , duration : Int
12 | , year : Maybe Int
13 | , artId : Maybe String
14 | }
15 |
16 |
17 | decode : Decoder AlbumSummary
18 | decode =
19 | map5 AlbumSummary
20 | (field "id" int)
21 | (field "name" string)
22 | (field "duration" int)
23 | (maybe <| field "year" int)
24 | (maybe <| field "coverArt" string)
25 |
26 |
27 | convert : AlbumSummary -> Entities.AlbumSummary.AlbumSummary
28 | convert album =
29 | { id = AlbumId album.id
30 | , name = album.name
31 | , duration = album.duration
32 | , year = album.year
33 | , artId = album.artId
34 | }
35 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/Artist.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.Artist exposing (Artist, convert, decode)
2 |
3 | import Artist.Types exposing (ArtistId(..))
4 | import Entities.Artist
5 | import Json.Decode exposing (Decoder, field, int, list, map3, string)
6 | import Socket.DTO.AlbumSummary exposing (AlbumSummary)
7 |
8 |
9 | type alias Artist =
10 | { id : Int
11 | , name : String
12 | , albums : List AlbumSummary
13 | }
14 |
15 |
16 | decode : Decoder Artist
17 | decode =
18 | map3 Artist
19 | (field "id" int)
20 | (field "name" string)
21 | (field "albums"
22 | (list Socket.DTO.AlbumSummary.decode)
23 | )
24 |
25 |
26 | convert : Artist -> Entities.Artist.Artist
27 | convert artist =
28 | { id = ArtistId artist.id
29 | , name = artist.name
30 | , albums = List.map Socket.DTO.AlbumSummary.convert artist.albums
31 | }
32 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/ArtistSummary.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.ArtistSummary exposing (ArtistSummary, convert, decode)
2 |
3 | import Artist.Types exposing (ArtistId(..))
4 | import Entities.ArtistSummary
5 | import Json.Decode exposing (Decoder, field, int, map2, string)
6 |
7 |
8 | type alias ArtistSummary =
9 | { id : Int
10 | , name : String
11 | }
12 |
13 |
14 | decode : Decoder ArtistSummary
15 | decode =
16 | map2 ArtistSummary
17 | (field "id" int)
18 | (field "name" string)
19 |
20 |
21 | convert : ArtistSummary -> Entities.ArtistSummary.ArtistSummary
22 | convert album =
23 | { id = ArtistId album.id
24 | , name = album.name
25 | }
26 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/Playlist.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.Playlist exposing (Playlist, convert, decode)
2 |
3 | import Entities.Playlist
4 | import Json.Decode exposing (Decoder, field, int, list, map3, string)
5 | import Socket.DTO.SongSummary exposing (SongSummary)
6 | import Song.Types exposing (SongId(..))
7 |
8 |
9 | type alias Playlist =
10 | { id : Int
11 | , name : String
12 | , songs : List SongSummary
13 | }
14 |
15 |
16 | decode : Decoder Playlist
17 | decode =
18 | map3 Playlist
19 | (field "id" int)
20 | (field "name" string)
21 | (field "songs" <| list Socket.DTO.SongSummary.decode)
22 |
23 |
24 | convert : Playlist -> Entities.Playlist.Playlist
25 | convert playlist =
26 | { id = playlist.id
27 | , name = playlist.name
28 | , songs = List.map (.id >> SongId) playlist.songs
29 | }
30 |
--------------------------------------------------------------------------------
/elm/src/Socket/DTO/SongSummary.elm:
--------------------------------------------------------------------------------
1 | module Socket.DTO.SongSummary exposing (SongSummary, convert, convertMany, decode)
2 |
3 | import Entities.SongSummary
4 | import Json.Decode exposing (Decoder, field, int, map3, string)
5 | import Song.Types exposing (SongId(..))
6 |
7 |
8 | type alias SongSummary =
9 | { id : Int
10 | , name : String
11 | , track : Int
12 | }
13 |
14 |
15 | decode : Decoder SongSummary
16 | decode =
17 | map3 SongSummary
18 | (field "id" int)
19 | (field "name" string)
20 | (field "track" int)
21 |
22 |
23 | convert : SongSummary -> Entities.SongSummary.SongSummary
24 | convert song =
25 | { id = SongId song.id
26 | , name = song.name
27 | , track = song.track
28 | }
29 |
30 |
31 | convertMany : List SongSummary -> List Entities.SongSummary.SongSummary
32 | convertMany list =
33 | List.map convert list
34 |
--------------------------------------------------------------------------------
/elm/src/Socket/Listeners/ScanStatus.elm:
--------------------------------------------------------------------------------
1 | module Socket.Listeners.ScanStatus exposing (listener)
2 |
3 | import Json.Decode exposing (bool, int)
4 | import Model exposing (Model)
5 | import Msg exposing (Msg)
6 | import Socket.NotificationListener exposing (NotificationListener, makeListenerWithParams)
7 | import Types exposing (Update)
8 |
9 |
10 | type alias Params =
11 | { count : Int
12 | , scanning : Bool
13 | }
14 |
15 |
16 | paramsDecoder : Json.Decode.Decoder Params
17 | paramsDecoder =
18 | Json.Decode.map2
19 | Params
20 | (Json.Decode.field "count" int)
21 | (Json.Decode.field "scanning" bool)
22 |
23 |
24 | listener : NotificationListener Model Msg
25 | listener =
26 | makeListenerWithParams paramsDecoder updater onError
27 |
28 |
29 | onError : String -> Update Model Msg
30 | onError err model =
31 | ( { model | message = "Couldn't understand scan status: " ++ err }, Cmd.none )
32 |
33 |
34 | updater : Params -> Update Model Msg
35 | updater params model =
36 | ( { model | isScanning = params.scanning, scanCount = params.count }, Cmd.none )
37 |
--------------------------------------------------------------------------------
/elm/src/Socket/Message.elm:
--------------------------------------------------------------------------------
1 | module Socket.Message exposing (Message(..), parse)
2 |
3 | import Json.Decode
4 | exposing
5 | ( Decoder
6 | , andThen
7 | , decodeString
8 | , errorToString
9 | , fail
10 | , field
11 | , map
12 | , oneOf
13 | , string
14 | )
15 | import Socket.Notification exposing (Notification)
16 | import Socket.Response exposing (Response)
17 |
18 |
19 | {-| A message received through a websocket.
20 | -}
21 | type Message
22 | = Response Response
23 | | Notification Notification
24 |
25 |
26 | {-| Try to parse a JSON string into a message.
27 | -}
28 | parse : String -> Result String Message
29 | parse =
30 | decodeString decode >> Result.mapError errorToString
31 |
32 |
33 | {-| Decode JSON into a message.
34 | -}
35 | decode : Decoder Message
36 | decode =
37 | field "jsonrpc" string
38 | |> andThen decodeInner
39 |
40 |
41 | decodeInner : String -> Decoder Message
42 | decodeInner version =
43 | case version of
44 | "2.0" ->
45 | oneOf
46 | [ map Response Socket.Response.decode
47 | , map Notification Socket.Notification.decode
48 | ]
49 |
50 | _ ->
51 | fail "Bad RPC version!"
52 |
--------------------------------------------------------------------------------
/elm/src/Socket/MessageId.elm:
--------------------------------------------------------------------------------
1 | module Socket.MessageId exposing (MessageId(..), getRawMessageId)
2 |
3 |
4 | type MessageId
5 | = MessageId Int
6 |
7 |
8 | getRawMessageId : MessageId -> Int
9 | getRawMessageId messageId =
10 | let
11 | (MessageId id) =
12 | messageId
13 | in
14 | id
15 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/GetAlbums.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.GetAlbums exposing (getAlbums)
2 |
3 | import Dict
4 | import Json.Decode exposing (field, list)
5 | import Model exposing (Model)
6 | import Msg exposing (Msg)
7 | import Socket.DTO.AlbumSummary exposing (AlbumSummary, convert, decode)
8 | import Socket.Listener exposing (Listener, makeIrresponsibleListener)
9 | import Socket.RequestData exposing (RequestData)
10 | import Types exposing (Update)
11 |
12 |
13 | type alias Body =
14 | { albums : List AlbumSummary }
15 |
16 |
17 | getAlbums : RequestData Model
18 | getAlbums =
19 | { method = "getAlbums"
20 | , params = Nothing
21 | , listener = Just onResponse
22 | }
23 |
24 |
25 | responseDecoder : Json.Decode.Decoder Body
26 | responseDecoder =
27 | Json.Decode.map Body
28 | (field "albums"
29 | (list decode)
30 | )
31 |
32 |
33 | onResponse : Listener Model Msg
34 | onResponse =
35 | makeIrresponsibleListener
36 | Nothing
37 | responseDecoder
38 | setAlbums
39 |
40 |
41 | setAlbums : Body -> Update Model Msg
42 | setAlbums body model =
43 | let
44 | tuples =
45 | List.map (\a -> ( a.id, convert a )) body.albums
46 | in
47 | ( { model | albums = Dict.fromList tuples }, Cmd.none )
48 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/GetArtists.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.GetArtists exposing (getArtists)
2 |
3 | import Dict
4 | import Json.Decode exposing (field, list)
5 | import Model exposing (Model)
6 | import Msg exposing (Msg)
7 | import Socket.DTO.ArtistSummary exposing (convert, decode)
8 | import Socket.Listener exposing (Listener, makeIrresponsibleListener)
9 | import Socket.RequestData exposing (RequestData)
10 | import Types exposing (Update)
11 |
12 |
13 | type alias Body =
14 | { artists : List Artist }
15 |
16 |
17 | type alias Artist =
18 | { id : Int
19 | , name : String
20 | }
21 |
22 |
23 | getArtists : RequestData Model
24 | getArtists =
25 | { method = "getArtists"
26 | , params = Nothing
27 | , listener = Just onResponse
28 | }
29 |
30 |
31 | responseDecoder : Json.Decode.Decoder Body
32 | responseDecoder =
33 | Json.Decode.map Body
34 | (field "artists"
35 | (list decode)
36 | )
37 |
38 |
39 | onResponse : Listener Model Msg
40 | onResponse =
41 | makeIrresponsibleListener
42 | Nothing
43 | responseDecoder
44 | setArtists
45 |
46 |
47 | setArtists : Body -> Update Model Msg
48 | setArtists body model =
49 | let
50 | tuples =
51 | List.map (\a -> ( a.id, convert a )) body.artists
52 | in
53 | ( { model | artists = Dict.fromList tuples }, Cmd.none )
54 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/GetPlaylists.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.GetPlaylists exposing (getPlaylists)
2 |
3 | import Dict
4 | import Json.Decode exposing (field, int, list, string)
5 | import Model exposing (Model)
6 | import Msg exposing (Msg)
7 | import Socket.Listener exposing (Listener, makeIrresponsibleListener)
8 | import Socket.RequestData exposing (RequestData)
9 | import Types exposing (Update)
10 |
11 |
12 | type alias Body =
13 | { playlists : List Playlist }
14 |
15 |
16 | type alias Playlist =
17 | { id : Int
18 | , name : String
19 | }
20 |
21 |
22 | getPlaylists : RequestData Model
23 | getPlaylists =
24 | { method = "getPlaylists"
25 | , params = Nothing
26 | , listener = Just onResponse
27 | }
28 |
29 |
30 | responseDecoder : Json.Decode.Decoder Body
31 | responseDecoder =
32 | Json.Decode.map Body
33 | (field "playlists"
34 | (list <|
35 | Json.Decode.map2 Playlist
36 | (field "id" int)
37 | (field "name" string)
38 | )
39 | )
40 |
41 |
42 | onResponse : Listener Model Msg
43 | onResponse =
44 | makeIrresponsibleListener
45 | Nothing
46 | responseDecoder
47 | setPlaylists
48 |
49 |
50 | setPlaylists : Body -> Update Model Msg
51 | setPlaylists body model =
52 | let
53 | tuples =
54 | List.map (\a -> ( a.id, a )) body.playlists
55 | in
56 | ( { model | playlists = Dict.fromList tuples }, Cmd.none )
57 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/Handshake.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.Handshake exposing (makeRequest, prepareRequest)
2 |
3 | import Json.Decode
4 | import Json.Encode
5 | import Model exposing (Model)
6 | import Msg exposing (Msg)
7 | import Socket.Listener exposing (Listener, makeIrresponsibleListener)
8 | import Socket.RequestData exposing (RequestData)
9 | import Types exposing (Update)
10 |
11 |
12 | type alias Body =
13 | { accepted : Bool }
14 |
15 |
16 | responseDecoder : Json.Decode.Decoder Body
17 | responseDecoder =
18 | Json.Decode.map Body
19 | (Json.Decode.field "accepted" Json.Decode.bool)
20 |
21 |
22 | makeRequest : String -> Json.Encode.Value
23 | makeRequest ticket =
24 | Json.Encode.object
25 | [ ( "ticket", Json.Encode.string ticket ) ]
26 |
27 |
28 | {-| Make a message which starts the websocket handshake.
29 | -}
30 | prepareRequest : String -> Update Model Msg -> RequestData Model
31 | prepareRequest ticket onHandshakeSuccess =
32 | { method = "handshake"
33 | , params = makeRequest ticket |> Just
34 | , listener = onResponse onHandshakeSuccess |> Just
35 | }
36 |
37 |
38 | onResponse : Update Model Msg -> Listener Model Msg
39 | onResponse onHandshakeSuccess =
40 | makeIrresponsibleListener
41 | Nothing
42 | responseDecoder
43 | (onSuccess onHandshakeSuccess)
44 |
45 |
46 | onSuccess : Update Model Msg -> Body -> Update Model Msg
47 | onSuccess onHandshakeSuccess response model =
48 | if response.accepted then
49 | onHandshakeSuccess { model | message = "Websocket handshake succeeded" }
50 |
51 | else
52 | ( { model | message = "Websocket handshake failed" }, Cmd.none )
53 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/Start.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.Start exposing (start)
2 |
3 | import Model exposing (Model, getSocketModel, setSocketModel)
4 | import Msg exposing (Msg)
5 | import Socket.Core exposing (sendMessage, sendQueuedMessage)
6 | import Socket.Methods.GetArtists exposing (getArtists)
7 | import Types exposing (Update, combineMany)
8 |
9 |
10 | {-| This should be run once the websocket handshake is complete.
11 | -}
12 | start : Update Model Msg
13 | start =
14 | combineMany
15 | [ setWebsocketOpen
16 | , processQueue
17 | , sendMessage getArtists False
18 | ]
19 |
20 |
21 | setWebsocketOpen : Update Model Msg
22 | setWebsocketOpen model =
23 | let
24 | socket =
25 | getSocketModel model
26 |
27 | updated =
28 | setSocketModel model { socket | isOpen = True }
29 | in
30 | ( updated, Cmd.none )
31 |
32 |
33 | processQueue : Update Model Msg
34 | processQueue model =
35 | combineMany
36 | (List.map sendQueuedMessage (getSocketModel model).messageQueue)
37 | model
38 |
--------------------------------------------------------------------------------
/elm/src/Socket/Methods/StartScan.elm:
--------------------------------------------------------------------------------
1 | module Socket.Methods.StartScan exposing (prepareRequest)
2 |
3 | import Json.Encode
4 | import Model exposing (Model)
5 | import Socket.RequestData exposing (RequestData)
6 |
7 |
8 | prepareRequest : Bool -> Bool -> RequestData Model
9 | prepareRequest shouldUpdate shouldDelete =
10 | { method = "startScan"
11 | , params = makeRequest shouldUpdate shouldDelete |> Just
12 | , listener = Nothing
13 | }
14 |
15 |
16 | makeRequest : Bool -> Bool -> Json.Encode.Value
17 | makeRequest shouldUpdate shouldDelete =
18 | Json.Encode.object
19 | [ ( "update", Json.Encode.bool shouldUpdate )
20 | , ( "delete", Json.Encode.bool shouldDelete )
21 | ]
22 |
--------------------------------------------------------------------------------
/elm/src/Socket/Model.elm:
--------------------------------------------------------------------------------
1 | module Socket.Model exposing (Model)
2 |
3 | import Dict exposing (Dict)
4 | import Msg exposing (Msg)
5 | import Socket.Listener exposing (Listener)
6 | import Socket.MessageId exposing (MessageId)
7 | import Socket.NotificationListener exposing (NotificationListener)
8 | import Socket.RequestData exposing (RequestData)
9 |
10 |
11 | type alias Model m =
12 | { listeners : Dict Int (Listener m Msg) -- Everything listening out for a server response, keyed by the id of the response they listen for.
13 | , notificationListeners : Dict String (NotificationListener m Msg) -- Everything listening out for server notifications, keyed by the notification method they listen for.
14 | , messageQueue : List ( MessageId, RequestData m ) -- Queue for messages which have arrived while the socket is closed
15 | , nextMessageId : MessageId -- The next unused ID for a message
16 | , isOpen : Bool -- True iff the socket is open and authenticated
17 | , ticket : Maybe String
18 | }
19 |
--------------------------------------------------------------------------------
/elm/src/Socket/Notification.elm:
--------------------------------------------------------------------------------
1 | module Socket.Notification exposing (Notification, decode)
2 |
3 | import Json.Decode exposing (Decoder, field, map2, maybe, string, value)
4 |
5 |
6 | {-| A websocket message which doesn't expect a reply.
7 | -}
8 | type alias Notification =
9 | { method : String
10 | , params : Maybe Json.Decode.Value
11 | }
12 |
13 |
14 | decode : Decoder Notification
15 | decode =
16 | map2 Notification
17 | (field "method" string)
18 | (maybe <| field "params" value)
19 |
--------------------------------------------------------------------------------
/elm/src/Socket/NotificationListener.elm:
--------------------------------------------------------------------------------
1 | module Socket.NotificationListener exposing (NotificationListener, makeListener, makeListenerWithParams)
2 |
3 | import Json.Decode exposing (Decoder, errorToString)
4 | import Socket.Notification exposing (Notification)
5 | import Types exposing (Update)
6 |
7 |
8 | type alias NotificationListener model msg =
9 | Notification -> Update model msg
10 |
11 |
12 | {-| Make a listener which cares about the notification parameters.
13 | -}
14 | makeListenerWithParams : Decoder a -> (a -> Update model msg) -> (String -> Update model msg) -> NotificationListener model msg
15 | makeListenerWithParams decode update onError notification model =
16 | case notification.params of
17 | Just params ->
18 | case Json.Decode.decodeValue decode params of
19 | Ok body ->
20 | update body model
21 |
22 | -- We got the wrong type of parameters
23 | Err error ->
24 | onError (errorToString error) model
25 |
26 | -- We expected parameters but didn't get any
27 | Nothing ->
28 | onError "No parameters received" model
29 |
30 |
31 | {-| Make a listener which doesn't care about parameters.
32 | Since the the notification is pre-routed based on its method
33 | this means it doesn't depend on the notification at all.
34 | -}
35 | makeListener : Update model msg -> NotificationListener model msg
36 | makeListener =
37 | always
38 |
--------------------------------------------------------------------------------
/elm/src/Socket/Request.elm:
--------------------------------------------------------------------------------
1 | module Socket.Request exposing (makeRequest)
2 |
3 | import Json.Encode exposing (Value, int, object, string)
4 | import Socket.MessageId exposing (MessageId, getRawMessageId)
5 |
6 |
7 | makeRequest : MessageId -> String -> Maybe Value -> Value
8 | makeRequest id method params =
9 | object
10 | [ ( "jsonrpc", string "2.0" )
11 | , ( "method", string method )
12 | , ( "params", maybeEncode params )
13 | , ( "id", int <| getRawMessageId id )
14 | ]
15 |
16 |
17 | maybeEncode : Maybe Value -> Value
18 | maybeEncode maybe =
19 | case maybe of
20 | Just value ->
21 | value
22 |
23 | Nothing ->
24 | Json.Encode.object []
25 |
--------------------------------------------------------------------------------
/elm/src/Socket/RequestData.elm:
--------------------------------------------------------------------------------
1 | module Socket.RequestData exposing (RequestData)
2 |
3 | import Json.Encode
4 | import Msg exposing (Msg)
5 | import Socket.Listener
6 |
7 |
8 | {-| Describes a message to send down the websocket and optionally how to handle a response to that message.
9 | -}
10 | type alias RequestData model =
11 | { method : String
12 | , params : Maybe Json.Encode.Value
13 | , listener : Maybe (Socket.Listener.Listener model Msg) -- How any replies to the message should be handled.
14 | }
15 |
--------------------------------------------------------------------------------
/elm/src/Socket/Response.elm:
--------------------------------------------------------------------------------
1 | module Socket.Response exposing (Response, decode)
2 |
3 | import Json.Decode exposing (Decoder, Value, andThen, field, int, map, oneOf, value)
4 | import Socket.MessageId exposing (MessageId(..))
5 |
6 |
7 | {-| A reply received through a websocket.
8 | The body represents either the success JSON or error JSON of a response.
9 | -}
10 | type alias Response =
11 | { id : MessageId
12 | , body : Result Value Value
13 | }
14 |
15 |
16 | decode : Decoder Response
17 | decode =
18 | field "id" int
19 | |> andThen decodeInner
20 |
21 |
22 | decodeInner : Int -> Decoder Response
23 | decodeInner id =
24 | let
25 | make =
26 | Response (MessageId id)
27 | in
28 | oneOf
29 | [ map (Err >> make) (field "error" value)
30 | , map (Ok >> make) (field "result" value)
31 | ]
32 |
--------------------------------------------------------------------------------
/elm/src/Socket/Select.elm:
--------------------------------------------------------------------------------
1 | module Socket.Select exposing (getListener, getNotificationListener)
2 |
3 | import Dict
4 | import Model
5 | import Msg exposing (Msg)
6 | import Socket.Listener exposing (Listener)
7 | import Socket.MessageId exposing (MessageId, getRawMessageId)
8 | import Socket.Model
9 | import Socket.NotificationListener exposing (NotificationListener)
10 |
11 |
12 | type alias Model =
13 | Socket.Model.Model Model.Model
14 |
15 |
16 | {-| Try and retrieve the listener with the given ID.
17 | -}
18 | getListener : MessageId -> Model -> Maybe (Listener Model.Model Msg)
19 | getListener id model =
20 | Dict.get (getRawMessageId id) model.listeners
21 |
22 |
23 | {-| Try and retrieve the notification listener for the given method.
24 | -}
25 | getNotificationListener : String -> Model -> Maybe (NotificationListener Model.Model Msg)
26 | getNotificationListener method model =
27 | Dict.get method model.notificationListeners
28 |
--------------------------------------------------------------------------------
/elm/src/Socket/SocketMsg.elm:
--------------------------------------------------------------------------------
1 | module Socket.SocketMsg exposing (SocketMsg(..))
2 |
3 |
4 | type SocketMsg
5 | = SocketOpened -- The websocket has been successfully opened
6 | | SocketClosed -- The websocket has been closed
7 | | SocketIn String -- A message has been received over the websocket
8 |
--------------------------------------------------------------------------------
/elm/src/Song/Select.elm:
--------------------------------------------------------------------------------
1 | module Song.Select exposing (getSong)
2 |
3 | import Audio.State exposing (State(..))
4 | import Dict
5 | import Entities.SongSummary exposing (SongSummary)
6 | import Loadable exposing (Loadable(..))
7 | import Model exposing (Model)
8 | import Routing exposing (Route(..))
9 | import Song.Types exposing (SongId(..))
10 |
11 |
12 | getSong : Model -> SongId -> Maybe SongSummary
13 | getSong model songId =
14 | let
15 | (SongId id) =
16 | songId
17 | in
18 | Dict.get id model.songs
19 |
--------------------------------------------------------------------------------
/elm/src/Song/Types.elm:
--------------------------------------------------------------------------------
1 | module Song.Types exposing (SongId(..), getRawSongId)
2 |
3 |
4 | type SongId
5 | = SongId Int
6 |
7 |
8 | getRawSongId : SongId -> Int
9 | getRawSongId songId =
10 | let
11 | (SongId raw) =
12 | songId
13 | in
14 | raw
15 |
--------------------------------------------------------------------------------
/elm/src/Types.elm:
--------------------------------------------------------------------------------
1 | module Types exposing (Update, UpdateWithReturn, combine, combineMany, noOp)
2 |
3 |
4 | type alias Update model msg =
5 | model -> ( model, Cmd msg )
6 |
7 |
8 | type alias UpdateWithReturn model msg return =
9 | model -> ( ( model, Cmd msg ), return )
10 |
11 |
12 | noOp : Update model msg
13 | noOp model =
14 | ( model, Cmd.none )
15 |
16 |
17 | {-| Running one update and then another.
18 | -}
19 | combine : Update model msg -> Update model msg -> Update model msg
20 | combine first second model =
21 | let
22 | ( modelA, cmdA ) =
23 | first model
24 |
25 | ( modelB, cmdB ) =
26 | second modelA
27 | in
28 | ( modelB, Cmd.batch [ cmdB, cmdA ] )
29 |
30 |
31 | {-| Running all the given updates in the order they appear in the list.
32 | -}
33 | combineMany : List (Update model msg) -> Update model msg
34 | combineMany updates =
35 | List.foldr combine noOp updates
36 |
--------------------------------------------------------------------------------
/elm/src/Updaters.elm:
--------------------------------------------------------------------------------
1 | module Updaters exposing
2 | ( logOut
3 | , onUrlChange
4 | )
5 |
6 | import Album.Fetch exposing (fetchAlbum)
7 | import Artist.Fetch exposing (fetchArtist)
8 | import Artist.Types exposing (ArtistId(..))
9 | import Audio.Select exposing (..)
10 | import Audio.State exposing (State(..))
11 | import Loadable exposing (Loadable(..))
12 | import Model exposing (Model)
13 | import Msg exposing (Msg)
14 | import Playlist.Fetch exposing (fetchPlaylist)
15 | import Ports
16 | import Routing exposing (Route(..))
17 | import Socket.Core exposing (sendMessage)
18 | import Socket.MessageId exposing (MessageId(..))
19 | import Socket.Methods.GetAlbums exposing (getAlbums)
20 | import Socket.Methods.GetArtists exposing (getArtists)
21 | import Socket.Methods.GetPlaylists exposing (getPlaylists)
22 | import Types exposing (Update)
23 | import Url exposing (Url)
24 |
25 |
26 | logOut : Update Model Msg
27 | logOut model =
28 | ( { model | username = "", token = Absent }, Ports.websocketClose () )
29 |
30 |
31 | onUrlChange : Url -> Update Model Msg
32 | onUrlChange url model =
33 | let
34 | m =
35 | { model | route = Routing.parseUrl url }
36 | in
37 | case m.route of
38 | Just (Artist id) ->
39 | fetchArtist Nothing id m
40 |
41 | Just (Album id) ->
42 | fetchAlbum Nothing id m
43 |
44 | Just (Playlist id) ->
45 | fetchPlaylist Nothing id m
46 |
47 | Just Playlists ->
48 | sendMessage getPlaylists False m
49 |
50 | Just Albums ->
51 | sendMessage getAlbums False m
52 |
53 | Just Artists ->
54 | sendMessage getArtists False m
55 |
56 | Nothing ->
57 | ( m, Cmd.none )
58 |
--------------------------------------------------------------------------------
/elm/src/Util.elm:
--------------------------------------------------------------------------------
1 | module Util exposing (insertMany)
2 |
3 | import Dict exposing (Dict)
4 |
5 |
6 | insertMany : (a -> comparable) -> (a -> value) -> List a -> Dict comparable value -> Dict comparable value
7 | insertMany toKey toValue list dict =
8 | List.foldl
9 | (\a -> \d -> Dict.insert (toKey a) (toValue a) d)
10 | dict
11 | list
12 |
--------------------------------------------------------------------------------
/elm/src/Views/Album.elm:
--------------------------------------------------------------------------------
1 | module Views.Album exposing (view)
2 |
3 | import Album.Select exposing (getAlbum, getAlbumArt, getAlbumSongs)
4 | import Album.Types exposing (AlbumId)
5 | import Audio.Msg exposing (AudioMsg(..))
6 | import Html.Styled exposing (Html, button, div, img, text)
7 | import Html.Styled.Attributes exposing (class, src)
8 | import Html.Styled.Events exposing (onClick)
9 | import Loadable exposing (Loadable(..))
10 | import Model exposing (Model)
11 | import Msg exposing (Msg(..))
12 | import Player.Msg exposing (PlayerMsg(..))
13 | import Views.Song
14 |
15 |
16 | view : AlbumId -> Model -> Html Msg
17 | view id model =
18 | case getAlbum id model of
19 | Absent ->
20 | div [] [ text "No album" ]
21 |
22 | Loading _ ->
23 | div [] [ text "Loading album" ]
24 |
25 | Loaded album ->
26 | div []
27 | [ div []
28 | [ div [] [ text album.name ]
29 | , img [ class "album__art", src <| getAlbumArt album.artId ] []
30 | , button [ onClick <| PlayerMsg (PlayAlbum id) ] [ text "Play album" ]
31 | ]
32 | , div [] <|
33 | List.map (Views.Song.view model) (getAlbumSongs album model)
34 | ]
35 |
--------------------------------------------------------------------------------
/elm/src/Views/Albums.elm:
--------------------------------------------------------------------------------
1 | module Views.Albums exposing (view)
2 |
3 | import Album.Types exposing (getRawAlbumId)
4 | import Dict
5 | import Entities.AlbumSummary exposing (AlbumSummaries)
6 | import Html.Styled exposing (Html, a, div, text)
7 | import Html.Styled.Attributes exposing (class, href)
8 | import Model exposing (Model)
9 | import Msg exposing (Msg(..))
10 | import String exposing (fromInt)
11 |
12 |
13 | view : Model -> Html Msg
14 | view model =
15 | div [ class "home__wrap" ]
16 | [ viewAlbums model.albums
17 | ]
18 |
19 |
20 | viewAlbums : AlbumSummaries -> Html msg
21 | viewAlbums albums =
22 | div [ class "home__artists" ]
23 | (List.map
24 | (\album ->
25 | div
26 | [ class "home__artist" ]
27 | [ a [ href <| "/album/" ++ fromInt (getRawAlbumId album.id) ] [ text album.name ] ]
28 | )
29 | (Dict.values albums)
30 | )
31 |
--------------------------------------------------------------------------------
/elm/src/Views/Artist.elm:
--------------------------------------------------------------------------------
1 | module Views.Artist exposing (view)
2 |
3 | import Artist.Select exposing (getArtist)
4 | import Artist.Types exposing (ArtistId)
5 | import Audio.Msg exposing (AudioMsg(..))
6 | import Html.Styled exposing (Html, div, text)
7 | import Html.Styled.Attributes exposing (class)
8 | import Loadable exposing (Loadable(..))
9 | import Model exposing (Model)
10 | import Msg exposing (Msg(..))
11 | import Views.MiniAlbum
12 |
13 |
14 | view : ArtistId -> Model -> Html Msg
15 | view artistId model =
16 | case getArtist artistId model of
17 | Absent ->
18 | div [] [ text "No artist" ]
19 |
20 | Loading _ ->
21 | div [] [ text "Loading artist" ]
22 |
23 | Loaded artist ->
24 | div []
25 | [ div [] [ text artist.name ]
26 | , div [ class "artist__albums" ] <|
27 | List.map
28 | Views.MiniAlbum.view
29 | artist.albums
30 | ]
31 |
--------------------------------------------------------------------------------
/elm/src/Views/Artists.elm:
--------------------------------------------------------------------------------
1 | module Views.Artists exposing (view)
2 |
3 | import Artist.Types exposing (getRawArtistId)
4 | import Dict
5 | import Entities.ArtistSummary exposing (ArtistSummaries)
6 | import Html.Styled exposing (Html, a, div, text)
7 | import Html.Styled.Attributes exposing (class, href)
8 | import Model exposing (Model)
9 | import Msg exposing (Msg(..))
10 | import String exposing (fromInt)
11 |
12 |
13 | view : Model -> Html Msg
14 | view model =
15 | div [ class "home__wrap" ]
16 | [ viewAlbums model.artists
17 | ]
18 |
19 |
20 | viewAlbums : ArtistSummaries -> Html msg
21 | viewAlbums albums =
22 | div [ class "home__artists" ]
23 | (List.map
24 | (\album ->
25 | div
26 | [ class "home__artist" ]
27 | [ a [ href <| "/artist/" ++ fromInt (getRawArtistId album.id) ] [ text album.name ] ]
28 | )
29 | (Dict.values albums)
30 | )
31 |
--------------------------------------------------------------------------------
/elm/src/Views/Home.elm:
--------------------------------------------------------------------------------
1 | module Views.Home exposing (view)
2 |
3 | import Html.Styled exposing (Html, button, div, input, label, span, text)
4 | import Html.Styled.Attributes exposing (checked, class, type_)
5 | import Html.Styled.Events exposing (onClick)
6 | import Model exposing (Model)
7 | import Msg exposing (Msg(..))
8 | import String exposing (fromInt)
9 |
10 |
11 | view : Model -> Html Msg
12 | view model =
13 | div [ class "home__wrap" ]
14 | [ span [] [ text <| "Scanned: " ++ String.fromInt model.scanCount ]
15 | , button [ onClick LogOut ] [ text "Log out" ]
16 | , checkboxInput "Update?" model.scanShouldUpdate ToggleScanUpdate
17 | , checkboxInput "Delete?" model.scanShouldDelete ToggleScanDelete
18 | , button [ onClick StartScan ] [ text "Start scan" ]
19 | ]
20 |
21 |
22 | checkboxInput : String -> Bool -> msg -> Html msg
23 | checkboxInput name isChecked msg =
24 | label [] [ input [ checked isChecked, type_ "checkbox", onClick msg ] [], text name ]
25 |
--------------------------------------------------------------------------------
/elm/src/Views/Login.elm:
--------------------------------------------------------------------------------
1 | module Views.Login exposing (view)
2 |
3 | import Html.Styled exposing (Html, button, div, form, input, text)
4 | import Html.Styled.Attributes exposing (class, disabled, name, placeholder, type_, value)
5 | import Html.Styled.Events exposing (onInput)
6 | import Json.Decode
7 | import Model exposing (Model)
8 | import Msg exposing (Msg(..))
9 |
10 |
11 | view : Model -> Html Msg
12 | view model =
13 | div [ class "login__wrap" ]
14 | [ form [ class "login__container" ]
15 | [ div [ class "login__logo " ] [ text "Sound." ]
16 | , viewInput "username" "text" "Username" model.username UsernameChanged
17 | , viewInput "password" "password" "Password" model.password PasswordChanged
18 | , button [ onClickNoBubble SubmitLogin, class "login__submit", disabled (model.username == "") ] [ text "Login" ]
19 | ]
20 | ]
21 |
22 |
23 | viewInput : String -> String -> String -> String -> (String -> msg) -> Html msg
24 | viewInput n t p v toMsg =
25 | input [ name n, type_ t, placeholder p, value v, onInput toMsg, class "login__input" ] []
26 |
27 |
28 | onClickNoBubble : msg -> Html.Styled.Attribute msg
29 | onClickNoBubble message =
30 | Html.Styled.Events.custom "click" (Json.Decode.succeed { message = message, stopPropagation = True, preventDefault = True })
31 |
--------------------------------------------------------------------------------
/elm/src/Views/MiniAlbum.elm:
--------------------------------------------------------------------------------
1 | module Views.MiniAlbum exposing (view)
2 |
3 | import Album.Select exposing (getAlbumArt)
4 | import Album.Types exposing (getRawAlbumId)
5 | import Audio.Msg exposing (AudioMsg(..))
6 | import Entities.AlbumSummary exposing (AlbumSummary)
7 | import Html.Styled exposing (Html, a, button, div, img, text)
8 | import Html.Styled.Attributes exposing (class, href, src)
9 | import Html.Styled.Events exposing (onClick)
10 | import Loadable exposing (Loadable(..))
11 | import Msg exposing (Msg(..))
12 | import Player.Msg exposing (PlayerMsg(..))
13 | import String exposing (fromInt)
14 |
15 |
16 | view : AlbumSummary -> Html Msg
17 | view album =
18 | div [ class "home__artist" ]
19 | [ div []
20 | [ img [ class "artist__album--art", src <| getAlbumArt album.artId ] []
21 | , a [ href <| "/album/" ++ fromInt (getRawAlbumId album.id) ] [ text album.name ]
22 | , button [ onClick <| PlayerMsg (PlayAlbum album.id) ] [ text "Play" ]
23 | ]
24 | ]
25 |
--------------------------------------------------------------------------------
/elm/src/Views/Playlist.elm:
--------------------------------------------------------------------------------
1 | module Views.Playlist exposing (view)
2 |
3 | import Audio.Msg exposing (AudioMsg(..))
4 | import Html.Styled exposing (Html, button, div, text)
5 | import Html.Styled.Events exposing (onClick)
6 | import Loadable exposing (Loadable(..))
7 | import Model exposing (Model)
8 | import Msg exposing (Msg(..))
9 | import Player.Msg exposing (PlayerMsg(..))
10 | import Playlist.Select exposing (getPlaylist, getPlaylistSongs)
11 | import Playlist.Types exposing (PlaylistId)
12 | import Views.Song
13 |
14 |
15 | view : PlaylistId -> Model -> Html Msg
16 | view playlistId model =
17 | case getPlaylist playlistId model of
18 | Absent ->
19 | div [] [ text "No playlist" ]
20 |
21 | Loading _ ->
22 | div [] [ text "Loading playlist" ]
23 |
24 | Loaded playlist ->
25 | div []
26 | [ div []
27 | [ div [] [ text playlist.name ]
28 | , button [ onClick <| PlayerMsg (PlayPlaylist playlistId) ] [ text "Play playlist" ]
29 | ]
30 | , div [] <|
31 | List.map (Views.Song.view model) (getPlaylistSongs playlist model)
32 | ]
33 |
--------------------------------------------------------------------------------
/elm/src/Views/PlaylistItem.elm:
--------------------------------------------------------------------------------
1 | module Views.PlaylistItem exposing (view)
2 |
3 | import Audio.Msg exposing (AudioMsg(..))
4 | import Html.Styled exposing (Html, button, div, text)
5 | import Html.Styled.Attributes exposing (class)
6 | import Html.Styled.Events exposing (onClick)
7 | import Loadable exposing (Loadable(..))
8 | import Model exposing (Model)
9 | import Msg exposing (Msg(..))
10 | import Player.Msg exposing (PlayerMsg(..))
11 | import Song.Select exposing (getSong)
12 | import Song.Types exposing (SongId)
13 |
14 |
15 | view : Model -> Int -> SongId -> Html Msg
16 | view model index songId =
17 | case getSong model songId of
18 | Just song ->
19 | div [ class "playlist__item" ]
20 | [ button [ onClick <| PlayerMsg (PlayItem index) ] [ text "Play" ]
21 | , div [] [ text song.name ]
22 | ]
23 |
24 | Nothing ->
25 | div [] [ text "Nothing" ]
26 |
--------------------------------------------------------------------------------
/elm/src/Views/Playlists.elm:
--------------------------------------------------------------------------------
1 | module Views.Playlists exposing (view)
2 |
3 | import Dict
4 | import Entities.PlaylistSummary exposing (PlaylistSummaries)
5 | import Html.Styled exposing (Html, a, div, text)
6 | import Html.Styled.Attributes exposing (class, href)
7 | import Model exposing (Model)
8 | import Msg exposing (Msg(..))
9 | import String exposing (fromInt)
10 |
11 |
12 | view : Model -> Html Msg
13 | view model =
14 | div [ class "home__wrap" ]
15 | [ viewPlaylists model.playlists
16 | ]
17 |
18 |
19 | viewPlaylists : PlaylistSummaries -> Html msg
20 | viewPlaylists playlists =
21 | div [ class "home__artists" ]
22 | (List.map
23 | (\playlist -> div [ class "home__artist" ] [ a [ href <| "/playlist/" ++ fromInt playlist.id ] [ text playlist.name ] ])
24 | (Dict.values playlists)
25 | )
26 |
--------------------------------------------------------------------------------
/elm/src/Views/Sidebar.elm:
--------------------------------------------------------------------------------
1 | module Views.Sidebar exposing (view)
2 |
3 | import Html.Styled exposing (Html, a, div, text)
4 | import Html.Styled.Attributes exposing (class, href)
5 | import Model exposing (Model)
6 | import Msg exposing (Msg(..))
7 |
8 |
9 | view : Model -> Html Msg
10 | view model =
11 | div
12 | []
13 | [ div
14 | [ class "app__header" ]
15 | [ a [ href "/" ] [ text "Home" ] ]
16 | , div
17 | [ class "app__header" ]
18 | [ a [ href "/artists" ] [ text "Artists" ] ]
19 | , div
20 | [ class "app__header" ]
21 | [ a [ href "/albums" ] [ text "Albums" ] ]
22 | , div
23 | [ class "app__header" ]
24 | [ a [ href "/playlists" ] [ text "Playlists" ] ]
25 | ]
26 |
--------------------------------------------------------------------------------
/elm/src/Views/Song.elm:
--------------------------------------------------------------------------------
1 | module Views.Song exposing (view)
2 |
3 | import Audio.Msg exposing (AudioMsg(..))
4 | import Entities.SongSummary exposing (SongSummary)
5 | import Html.Styled exposing (Html, button, div, text)
6 | import Html.Styled.Attributes exposing (class)
7 | import Html.Styled.Events exposing (onClick)
8 | import Loadable exposing (Loadable(..))
9 | import Model exposing (Model)
10 | import Msg exposing (Msg(..))
11 | import Player.Msg exposing (PlayerMsg(..))
12 | import String exposing (fromInt)
13 |
14 |
15 | view : Model -> Maybe SongSummary -> Html Msg
16 | view model maybeSong =
17 | case maybeSong of
18 | Just song ->
19 | div [ class "album__song" ]
20 | [ button [ onClick <| PlayerMsg (Play song.id) ] [ text "Play" ]
21 | , button [ onClick <| PlayerMsg (Queue song.id) ] [ text "Queue" ]
22 | , div [] [ text <| fromInt song.track ]
23 | , div [] [ text song.name ]
24 | ]
25 |
26 | Nothing ->
27 | div [ class "album__song" ] []
28 |
--------------------------------------------------------------------------------
/elm/src/css/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 | /* HTML5 display-role reset for older browsers */
95 | article,
96 | aside,
97 | details,
98 | figcaption,
99 | figure,
100 | footer,
101 | header,
102 | hgroup,
103 | menu,
104 | nav,
105 | section {
106 | display: block;
107 | }
108 | body {
109 | line-height: 1;
110 | }
111 | ol,
112 | ul {
113 | list-style: none;
114 | }
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 | blockquote:before,
120 | blockquote:after,
121 | q:before,
122 | q:after {
123 | content: "";
124 | content: none;
125 | }
126 | table {
127 | border-collapse: collapse;
128 | border-spacing: 0;
129 | }
130 |
--------------------------------------------------------------------------------
/elm/src/sass/_album.scss:
--------------------------------------------------------------------------------
1 | .album {
2 | &__song {
3 | display: flex;
4 | }
5 |
6 | &__art {
7 | width: 8rem;
8 | height: 8rem;
9 | object-fit: contain;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/elm/src/sass/_app.scss:
--------------------------------------------------------------------------------
1 | .app {
2 | &__wrap {
3 | display: grid;
4 | grid-template-areas:
5 | "header header"
6 | "side main"
7 | "playlist playlist"
8 | "player player";
9 | grid-template-rows: auto 1fr auto auto;
10 | grid-template-columns: auto 1fr;
11 |
12 | overflow: hidden;
13 | }
14 |
15 | &__header {
16 | grid-area: header;
17 | }
18 |
19 | &__main {
20 | grid-area: main;
21 | overflow-y: auto;
22 | }
23 |
24 | &__side {
25 | grid-area: side;
26 | }
27 |
28 | &__playlist {
29 | grid-area: playlist;
30 | max-height: 30vh;
31 | overflow: auto;
32 | background-color: black;
33 | color: white;
34 | }
35 |
36 | &__player {
37 | grid-area: player;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/elm/src/sass/_home.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | &__wrap {
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | &__artist {
8 | display: inline-block;
9 | padding: 0.1rem 0.2rem;
10 | text-decoration: underline;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/elm/src/sass/_login.scss:
--------------------------------------------------------------------------------
1 | .login {
2 | &__wrap {
3 | width: 100vw;
4 | height: 100vh;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | box-sizing: border-box;
9 | }
10 |
11 | &__container {
12 | width: 20rem;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | box-sizing: border-box;
17 | flex-direction: column;
18 | }
19 |
20 | &__input {
21 | padding: 1rem;
22 | font-size: 1rem;
23 | width: 100%;
24 | margin: 0.5rem;
25 | border: 1px #808080 solid;
26 | border-radius: 0.3rem;
27 | }
28 |
29 | &__submit {
30 | padding: 1.5rem;
31 | font-size: 0.9rem;
32 | width: 100%;
33 | margin-top: 1rem;
34 | border: 1px #808080 solid;
35 | border-radius: 0.3rem;
36 | color: white;
37 | background-color: #2e2e2e;
38 | }
39 |
40 | &__logo {
41 | font-family: "Shrikhand", cursive;
42 | font-size: 4rem;
43 | color: #2e2e2e;
44 | padding-bottom: 1rem;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/elm/src/sass/_player.scss:
--------------------------------------------------------------------------------
1 | .player {
2 | &__wrap {
3 | display: flex;
4 | flex-direction: column;
5 |
6 | background-color: black;
7 | color: white;
8 |
9 | padding: 1rem;
10 | }
11 |
12 | &__slider {
13 | &--wrap {
14 | width: 100%;
15 | background-color: blue;
16 | height: 1rem;
17 |
18 | position: relative;
19 | }
20 |
21 | &--elapsed {
22 | position: absolute;
23 | top: 0;
24 | bottom: 0;
25 | left: 0;
26 |
27 | background-color: red;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/elm/src/sass/_playlist.scss:
--------------------------------------------------------------------------------
1 | .playlist {
2 | &__items {
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | &__item {
8 | display: flex;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/elm/src/sass/artist.scss:
--------------------------------------------------------------------------------
1 | .artist {
2 | &__albums {
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | &__album {
8 | &--art {
9 | width: 3rem;
10 | height: 3rem;
11 | object-fit: contain;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/elm/src/sass/styles.scss:
--------------------------------------------------------------------------------
1 | // Google Fonts
2 | @import url(https://fonts.googleapis.com/css?family=Roboto);
3 | @import url(https://fonts.googleapis.com/css?family=Shrikhand);
4 |
5 | @import "app";
6 | @import "login";
7 | @import "home";
8 | @import "playlist";
9 | @import "album";
10 | @import "artist";
11 | @import "player";
12 |
13 | html * {
14 | box-sizing: border-box !important;
15 | font-family: "Roboto", sans-serif;
16 | }
17 |
--------------------------------------------------------------------------------
/elm/webpack.common.js:
--------------------------------------------------------------------------------
1 | module.exports = optimize => ({
2 | entry: "./index.js",
3 |
4 | output: {
5 | path: __dirname + "/dist",
6 | filename: "index.js"
7 | },
8 |
9 | resolve: { extensions: [".elm", ".js", ".scss", ".css"] },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.html$/,
14 | exclude: /node_modules/,
15 | use: {
16 | loader: "file-loader",
17 | options: {
18 | name: "[name].[ext]"
19 | }
20 | }
21 | },
22 | {
23 | test: /\.elm$/,
24 | exclude: [/elm-stuff/, /node_modules/],
25 | use: {
26 | loader: "elm-webpack-loader",
27 | options: {
28 | cwd: __dirname,
29 | cache: false,
30 | optimize
31 | }
32 | }
33 | },
34 | {
35 | test: /\.scss$/,
36 | use: [
37 | "style-loader", // creates style nodes from JS strings
38 | "css-loader", // translates CSS into CommonJS
39 | "sass-loader" // compiles Sass to CSS, using Node Sass by default
40 | ]
41 | },
42 | {
43 | test: /\.css$/,
44 | use: [
45 | "style-loader", // creates style nodes from JS strings
46 | "css-loader" // translates CSS into CommonJS
47 | ]
48 | }
49 | ],
50 | noParse: /\.elm$/
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/elm/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const common = require("./webpack.common");
2 | const merge = require("webpack-merge");
3 | const path = require("path");
4 |
5 | module.exports = merge(common(false), {
6 | mode: "development",
7 | devtool: "inline-source-map",
8 | devServer: {
9 | contentBase: path.join(__dirname, "dist"),
10 | port: 9000,
11 | historyApiFallback: true,
12 | proxy: {
13 | "/api": {
14 | target: "http://localhost:3684"
15 | },
16 | "/ws": {
17 | target: "http://localhost:3684",
18 | ws: true
19 | }
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/elm/webpack.home.js:
--------------------------------------------------------------------------------
1 | const common = require("./webpack.common");
2 | const merge = require("webpack-merge");
3 | const path = require("path");
4 |
5 | module.exports = merge(common(false), {
6 | mode: "development",
7 | devtool: "inline-source-map",
8 | devServer: {
9 | contentBase: path.join(__dirname, "dist"),
10 | port: 9000,
11 | historyApiFallback: true,
12 | proxy: {
13 | "/api": {
14 | target: "http://192.168.1.77:7071" // "http://localhost:3684"
15 | },
16 | "/ws": {
17 | target: "http://192.168.1.77:7071", // "http://localhost:3684"
18 | ws: true
19 | }
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/elm/webpack.live.js:
--------------------------------------------------------------------------------
1 | const common = require("./webpack.common");
2 | const merge = require("webpack-merge");
3 | const path = require("path");
4 |
5 | module.exports = merge(common(false), {
6 | mode: "development",
7 | devtool: "inline-source-map",
8 | devServer: {
9 | contentBase: path.join(__dirname, "dist"),
10 | port: 9000,
11 | historyApiFallback: true,
12 | proxy: {
13 | "/api": {
14 | target: "http://hednowley.synology.me:171"
15 | },
16 | "/ws": {
17 | target: "http://hednowley.synology.me:171",
18 | ws: true
19 | }
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/elm/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const common = require("./webpack.common");
2 | const merge = require("webpack-merge");
3 |
4 | module.exports = merge(common(true), {
5 | mode: "production"
6 | });
7 |
--------------------------------------------------------------------------------
/go/.gitignore:
--------------------------------------------------------------------------------
1 | /sound.exe
2 | /sound
3 | /debug
4 | /dto/*.test
5 | /handler/debug.test
6 | /dal/*.test
7 | *.test
8 | *.log
9 | __debug_bin
10 |
--------------------------------------------------------------------------------
/go/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch",
9 | "type": "go",
10 | "request": "launch",
11 | "mode": "auto",
12 | "program": "${workspaceRoot}",
13 | "env": {},
14 | "args": []
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/go/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "go.docsTool": "gogetdoc",
3 | "go.formatFlags": []
4 | }
5 |
--------------------------------------------------------------------------------
/go/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Run all tests",
8 | "type": "shell",
9 | "command": "go test -p 1 ./...",
10 | "problemMatcher": "$go"
11 | },
12 | {
13 | "label": "Format all go",
14 | "type": "shell",
15 | "command": " gofmt -s -w ./"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/go/api/api/api.go:
--------------------------------------------------------------------------------
1 | // Package api is stuff.
2 | package api
3 |
4 | import (
5 | "net/http"
6 |
7 | "github.com/hednowley/sound/config"
8 | )
9 |
10 | type ControllerContext struct {
11 | // Pointer to a DTO struct. This struct is kept in a closure along
12 | // with the Run func to make Run like a generic function.
13 | // The struct must be mutated rather than reassigning the pointer.
14 | Body interface{}
15 |
16 | // Run the controller action.
17 | Run func(user *config.User, w http.ResponseWriter, r *http.Request) *Response
18 | }
19 |
20 | // Controller is a web controller.
21 | // It accepts a data-transfer object and returns an unserialised Response.
22 | type Controller struct {
23 |
24 | // Request token will be authenticated iff this is true
25 | Secure bool
26 |
27 | // Create a new context for an incoming request.
28 | Make func() *ControllerContext
29 | }
30 |
31 | // BinaryController is a low-level web controller.
32 | // It accepts a data-transfer object, a ResponseWriter and a Request.
33 | // It returns a nil pointer to indicate that no further response is needed,
34 | // otherwise it returns an unserialised Response.
35 | type BinaryController struct {
36 | Secure bool // Request token will be authenticated iff this is true
37 | Run func(http.ResponseWriter, *http.Request, *config.User) *Response // Run the controller action.
38 | }
39 |
--------------------------------------------------------------------------------
/go/api/api/response.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | type Status int
4 |
5 | const (
6 | Success Status = 0
7 | Fail Status = 1
8 | Error Status = 2
9 | )
10 |
11 | // Response follows JSend syntax.
12 | type Response struct {
13 | Body interface{}
14 | Status Status
15 | }
16 |
17 | func NewErrorReponse(message string) *Response {
18 | return &Response{
19 | Body: message,
20 | Status: Error,
21 | }
22 | }
23 |
24 | func NewSuccessfulReponse(body interface{}) *Response {
25 | return &Response{
26 | Body: body,
27 | Status: Success,
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/go/api/api/spa.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | // ServeSinglePageApp serves static files if they exist, otherwise it serves a single HTML page.
10 | func ServeSinglePageApp(dir string, html string) func(w http.ResponseWriter, r *http.Request) {
11 | return func(w http.ResponseWriter, r *http.Request) {
12 |
13 | path, err := filepath.Abs(r.URL.Path)
14 | if err != nil {
15 | http.Error(w, err.Error(), http.StatusBadRequest)
16 | return
17 | }
18 |
19 | _, err = os.Stat(filepath.Join(dir, path))
20 | if os.IsNotExist(err) {
21 | // Serve the index file
22 | http.ServeFile(w, r, html)
23 | return
24 | } else if err != nil {
25 | // Some other error
26 | http.Error(w, err.Error(), http.StatusInternalServerError)
27 | return
28 | }
29 |
30 | // Requested file must exist so serve it
31 | http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/go/api/controller/art.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/config"
8 | "github.com/hednowley/sound/dal"
9 | "github.com/hednowley/sound/util"
10 | )
11 |
12 | // NewArtController creates a controller for serving artwork images.
13 | func NewArtController(dal *dal.DAL) *api.BinaryController {
14 |
15 | run := func(w http.ResponseWriter, r *http.Request, _ *config.User) *api.Response {
16 |
17 | params := r.URL.Query()
18 | id := params.Get("id")
19 |
20 | sizeParam := params.Get("size")
21 | size := util.ParseUint(sizeParam, 0)
22 |
23 | path := dal.GetArt(id, size)
24 | if path == nil {
25 | return api.NewErrorReponse("Art is not available")
26 | }
27 |
28 | http.ServeFile(w, r, *path)
29 | return nil
30 | }
31 |
32 | return &api.BinaryController{
33 | Run: run,
34 | Secure: true,
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/go/api/controller/authenticate.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/api/dto"
8 | "github.com/hednowley/sound/config"
9 | "github.com/hednowley/sound/services"
10 | )
11 |
12 | // NewAuthenticateController makes a controller which gives out JWT tokens in return for credentials.
13 | func NewAuthenticateController(authenticator *services.Authenticator) *api.Controller {
14 |
15 | make := func() *api.ControllerContext {
16 |
17 | credentials := &dto.Credentials{}
18 |
19 | run := func(_ *config.User, w http.ResponseWriter, r *http.Request) *api.Response {
20 |
21 | if authenticator.AuthenticateFromPassword(credentials.Username, credentials.Password) == nil {
22 | return api.NewErrorReponse("Bad credentials.")
23 | }
24 |
25 | token, err := authenticator.MakeJWT(credentials.Username)
26 | if err != nil {
27 | return api.NewErrorReponse("Could not make token.")
28 | }
29 |
30 | //expire := time.Now().AddDate(0, 0, 1)
31 | cookie := http.Cookie{
32 | Name: "token",
33 | Value: token,
34 | //Domain: "false",
35 | //Expires: expire,
36 | }
37 | http.SetCookie(w, &cookie)
38 |
39 | return api.NewSuccessfulReponse(&struct{}{})
40 | }
41 |
42 | return &api.ControllerContext{
43 | Body: credentials,
44 | Run: run,
45 | }
46 | }
47 |
48 | return &api.Controller{
49 | Make: make,
50 | Secure: false,
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/go/api/controller/stream.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "path"
6 | "strings"
7 |
8 | "github.com/hednowley/sound/api/api"
9 | "github.com/hednowley/sound/config"
10 | "github.com/hednowley/sound/dal"
11 | "github.com/hednowley/sound/util"
12 | )
13 |
14 | // NewStreamController creates a controller for streaming audio.
15 | func NewStreamController(dal *dal.DAL) *api.BinaryController {
16 |
17 | run := func(w http.ResponseWriter, r *http.Request, _ *config.User) *api.Response {
18 |
19 | params := r.URL.Query()
20 | idStr := params.Get("id")
21 | id := util.ParseUint(idStr, 0)
22 |
23 | if id == 0 {
24 | return api.NewErrorReponse("No ID!")
25 | }
26 |
27 | conn, err := dal.Db.GetConn()
28 | if err != nil {
29 | return api.NewErrorReponse(err.Error())
30 | }
31 | defer conn.Release()
32 |
33 | file, err := dal.Db.GetSong(conn, id)
34 | if err != nil {
35 | return api.NewErrorReponse(err.Error())
36 | }
37 |
38 | // http.ServeFile incorrectly guesses the Content-Type as "video/mp4" for AAC files
39 | // so we override it here.
40 | ext := strings.ToLower(path.Ext(file.Path))
41 | if ext == ".aac" || ext == ".m4a" {
42 | w.Header()["Content-Type"] = []string{"audio/aac"}
43 | }
44 |
45 | http.ServeFile(w, r, file.Path)
46 | return nil
47 | }
48 |
49 | return &api.BinaryController{
50 | Run: run,
51 | Secure: true,
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/go/api/controller/ticket.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/api/dto"
8 | "github.com/hednowley/sound/config"
9 | "github.com/hednowley/sound/socket"
10 | )
11 |
12 | // NewTicketController makes a controller which returns a new Websocket ticket.
13 | func NewTicketController(ticketer *socket.Ticketer) *api.Controller {
14 |
15 | make := func() *api.ControllerContext {
16 |
17 | return &api.ControllerContext{
18 | Body: nil,
19 | Run: func(user *config.User, _ http.ResponseWriter, _ *http.Request) *api.Response {
20 | r := dto.NewTicket(ticketer.MakeTicket(user))
21 | return api.NewSuccessfulReponse(&r)
22 | },
23 | }
24 | }
25 |
26 | return &api.Controller{
27 | Secure: true,
28 | Make: make,
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/go/api/dto/credentials.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | // Credentials are data from a login form.
4 | type Credentials struct {
5 | Username string `json:"username"`
6 | Password string `json:"password"`
7 | }
8 |
--------------------------------------------------------------------------------
/go/api/dto/ticket.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | // Ticket is a ticket which allows a new websocket session to be negotiated.
4 | type Ticket struct {
5 | Ticket string `json:"ticket"`
6 | }
7 |
8 | // NewTicket makes a new ticket.
9 | func NewTicket(ticket string) Ticket {
10 | return Ticket{
11 | Ticket: ticket,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/go/api/dto/token.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | // Token is a JSON Web Token.
4 | type Token struct {
5 | Token string `json:"token"`
6 | }
7 |
--------------------------------------------------------------------------------
/go/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "io/ioutil"
5 |
6 | "gopkg.in/yaml.v2"
7 | )
8 |
9 | // Config is a data object which holds the app's settings.
10 | type Config struct {
11 | Port int
12 | ArtPath string `yaml:"art path"`
13 | ArtSizes []uint `yaml:"art sizes"`
14 | ResizeArt bool `yaml:"resize art"`
15 | Db string
16 | MigrationsPath string `yaml:"migrations path"`
17 | LogConfig string `yaml:"log config"`
18 | IgnoredArticles []string `yaml:"ignored articles"`
19 | Users []User
20 | Secret string
21 | AccessControlAllowOrigin string `yaml:"access control allow origin"`
22 | WebsocketTicketExpiry int `yaml:"websocket ticket expiry"`
23 | BeetsProviders []BeetsProvider `yaml:"beets"`
24 | FileSystemProviders []FileSystemProvider `yaml:"filesystem"`
25 | }
26 |
27 | type BeetsProvider struct {
28 | Database string
29 | Name string
30 | }
31 |
32 | type FileSystemProvider struct {
33 | Path string
34 | Name string
35 | Extensions []string
36 | }
37 |
38 | type User struct {
39 | Username string
40 | Password string
41 | Email string
42 | }
43 |
44 | // NewConfig creates config from the YAML at the provided path.
45 | // It's defined as a nullary function so we can inject the path before passing
46 | // the constructor to FX.
47 | func NewConfig(path string) func() (*Config, error) {
48 | return func() (*Config, error) {
49 | yamlData, err := ioutil.ReadFile(path)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | var c Config
55 | err = yaml.Unmarshal(yamlData, &c)
56 | return &c, err
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/go/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config_test
2 |
3 | import (
4 | "path/filepath"
5 | "runtime"
6 | "testing"
7 |
8 | "github.com/hednowley/sound/config"
9 | )
10 |
11 | func TestConfig(t *testing.T) {
12 | // Get test data path (found relative to this file)
13 | _, filename, _, _ := runtime.Caller(0)
14 | dir := filepath.Dir(filename)
15 | dataPath := filepath.Join(dir, "..", "testdata", "config.yaml")
16 |
17 | c, err := config.NewConfig(dataPath)()
18 |
19 | if err != nil {
20 | t.Error()
21 | }
22 |
23 | if len(c.ArtSizes) != 2 {
24 | t.Error()
25 | }
26 |
27 | if c.Port != 3684 ||
28 | c.Secret != "changeme" ||
29 | c.ArtPath != "~/temp/art" ||
30 | c.Db != "host=localhost port=5432 user=postgres password=sound dbname=sound sslmode=disable" ||
31 | c.LogConfig != "log-config.xml" ||
32 | c.AccessControlAllowOrigin != "*" ||
33 | c.WebsocketTicketExpiry != 30 {
34 | t.Error()
35 | }
36 |
37 | if len(c.FileSystemProviders) != 2 {
38 | t.Error()
39 | }
40 |
41 | if c.FileSystemProviders[0].Path != "~/temp/my music" ||
42 | c.FileSystemProviders[0].Name != "Gertrude's bangers" ||
43 | len(c.FileSystemProviders[0].Extensions) != 2 ||
44 | c.FileSystemProviders[0].Extensions[0] != "mp3" ||
45 | c.FileSystemProviders[0].Extensions[1] != "flac" {
46 | t.Error()
47 | }
48 |
49 | if len(c.BeetsProviders) != 1 ||
50 | c.BeetsProviders[0].Database != "~/beets/beetslib.blb" ||
51 | c.BeetsProviders[0].Name != "My beets music" {
52 | t.Error()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/go/dal/mock.go:
--------------------------------------------------------------------------------
1 | package dal
2 |
3 | import (
4 | "github.com/hednowley/sound/database"
5 | )
6 |
7 | func NewMock() *DAL {
8 | return &DAL{
9 | Db: database.NewMock(),
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/go/dao/album.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // Album is an album.
8 | type Album struct {
9 | ID uint
10 | ArtistID uint
11 | Name string
12 |
13 | Created *time.Time
14 | Arts []string
15 | Genres []string
16 | Years []int
17 | Duration int
18 | Disambiguator string // Two albums are only considered the same if their Name, Artist and Disambiguator are the same.
19 | Starred bool
20 |
21 | SongCount uint
22 | ArtistName string
23 | }
24 |
25 | func (a *Album) GetArt() string {
26 | if len(a.Arts) > 0 {
27 | return a.Arts[0]
28 | }
29 |
30 | return ""
31 | }
32 |
33 | func (a *Album) GetGenre() string {
34 | if len(a.Genres) > 0 {
35 | return a.Genres[0]
36 | }
37 |
38 | return ""
39 | }
40 |
41 | func (a *Album) GetYear() int {
42 | if len(a.Years) > 0 {
43 | return a.Years[0]
44 | }
45 |
46 | return 0
47 | }
48 |
--------------------------------------------------------------------------------
/go/dao/albumList2Type.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // AlbumList2Type is all they ways the results of AlbumList2 can be sorted: http://www.subsonic.org/pages/api.jsp#getAlbumList2
4 | type AlbumList2Type int
5 |
6 | // One of the ways the results of AlbumList2 can be sorted.
7 | const (
8 | Random AlbumList2Type = 0
9 | Newest AlbumList2Type = 1
10 | Frequent AlbumList2Type = 2
11 | Recent AlbumList2Type = 3
12 | Starred AlbumList2Type = 4
13 | AlphabeticalByName AlbumList2Type = 5
14 | AlphabeticalByArtist AlbumList2Type = 6
15 | ByYear AlbumList2Type = 7
16 | ByGenre AlbumList2Type = 8
17 | )
18 |
--------------------------------------------------------------------------------
/go/dao/art.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // Art is a single image of album art.
4 | type Art struct {
5 | ID uint
6 | Hash string
7 | Path string
8 | }
9 |
--------------------------------------------------------------------------------
/go/dao/artist.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // Artist is an artist.
4 | type Artist struct {
5 | ID uint
6 | Name string
7 | Arts []string
8 | Starred bool
9 |
10 | Duration int
11 | AlbumCount uint
12 | }
13 |
14 | func (a *Artist) GetArt() string {
15 | if len(a.Arts) > 0 {
16 | return a.Arts[0]
17 | }
18 |
19 | return ""
20 | }
21 |
--------------------------------------------------------------------------------
/go/dao/errNotFound.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // ErrNotFound is a special type of error for when data is missing.
4 | type ErrNotFound struct{}
5 |
6 | func (e *ErrNotFound) Error() string {
7 | return "Not found"
8 | }
9 |
10 | // IsErrNotFound check is an error is an ErrNotFound.
11 | func IsErrNotFound(e error) bool {
12 | _, ok := e.(*ErrNotFound)
13 | return ok
14 | }
15 |
--------------------------------------------------------------------------------
/go/dao/genre.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // Genre is a genre.
4 | type Genre struct {
5 | ID uint
6 | Name string
7 | SongCount int
8 | AlbumCount int
9 | }
10 |
--------------------------------------------------------------------------------
/go/dao/playlist.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // Playlist is a playlist.
8 | type Playlist struct {
9 | ID uint
10 | Name string
11 | Comment string
12 | Public bool
13 | Created *time.Time
14 | Changed *time.Time
15 | Duration int
16 | Owner string
17 |
18 | EntryCount int
19 | }
20 |
--------------------------------------------------------------------------------
/go/dao/playlistEntry.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | // PlaylistEntry is a single instance of a song inside a playlist.
4 | type PlaylistEntry struct {
5 | ID uint
6 | PlaylistID uint
7 | SongID uint
8 | Index int
9 | }
10 |
--------------------------------------------------------------------------------
/go/dao/song.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import "time"
4 |
5 | // Song is a song.
6 | type Song struct {
7 | ID uint
8 | Artist string // Name of the artist of the song. Can differ from the album's artist.
9 | AlbumID uint
10 | Path string
11 | Title string
12 | Track int
13 | Disc int
14 | Year int
15 | Art string
16 | Created *time.Time
17 | Size int64 // File size in bytes
18 | Bitrate int // Bitrate in kb/s
19 | Duration int // Duration in seconds
20 | Token string // An ID unique to this song amongst other songs from its provider
21 | ProviderID string // THe ID of the provider which supplied this song
22 | Starred bool
23 |
24 | // Precalculated fields which are stored for performance
25 | AlbumName string
26 | AlbumArtistID uint
27 | GenreName string
28 | }
29 |
--------------------------------------------------------------------------------
/go/database/mock.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "database/sql"
5 | "log"
6 | "path/filepath"
7 |
8 | "github.com/go-testfixtures/testfixtures/v3"
9 | "github.com/hednowley/sound/config"
10 | "github.com/hednowley/sound/projectpath"
11 | )
12 |
13 | // NewMock makes a new database seeded from test data.
14 | // Note that a real database must exist at the connection string below.
15 | func NewMock() *Default {
16 |
17 | conn := "host=localhost port=5432 user=sound password=sound dbname=sound_test sslmode=disable"
18 | mig := filepath.Join(projectpath.Root, "migrations")
19 |
20 | database, err := NewDefault(&config.Config{Db: conn, MigrationsPath: mig})
21 | if err != nil {
22 | log.Fatal(err)
23 | }
24 |
25 | // Open the database
26 | db, err := sql.Open("postgres", conn)
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 |
31 | dataDir := filepath.Join(projectpath.Root, "testdata", "dao")
32 |
33 | // Insert test data
34 | fixtures, err := testfixtures.New(
35 | testfixtures.Database(db),
36 | testfixtures.Dialect("postgres"),
37 | testfixtures.Directory(dataDir),
38 | testfixtures.ResetSequencesTo(10000),
39 | )
40 | if err != nil {
41 | log.Fatal(err)
42 | }
43 |
44 | err = fixtures.Load()
45 | if err != nil {
46 | log.Fatal(err)
47 | }
48 |
49 | return database
50 | }
51 |
--------------------------------------------------------------------------------
/go/entities/fileData.go:
--------------------------------------------------------------------------------
1 | package entities
2 |
3 | type FileInfo struct {
4 | Path string
5 | Artist string
6 | Album string
7 | AlbumArtist string
8 | Title string
9 | Genre string
10 | Track int
11 | Disc int
12 | Year int
13 | CoverArt *CoverArtData
14 | Size int64
15 | Bitrate int // Bitrate in kb/s
16 | Duration int // Duration in seconds
17 | Disambiguator string
18 | }
19 |
20 | type CoverArtData struct {
21 | Extension string
22 | Raw []byte
23 | }
24 |
--------------------------------------------------------------------------------
/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hednowley/sound
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
8 | github.com/dhowden/tag v0.0.0-20200412032933-5d76b8eaae27
9 | github.com/go-testfixtures/testfixtures/v3 v3.1.2
10 | github.com/golang-migrate/migrate/v4 v4.11.0
11 | github.com/google/uuid v1.1.1
12 | github.com/gorilla/mux v1.7.4
13 | github.com/gorilla/websocket v1.4.2
14 | github.com/jackc/pgx/v4 v4.6.0
15 | github.com/lib/pq v1.5.2
16 | github.com/mattn/go-sqlite3 v2.0.3+incompatible
17 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
18 | go.uber.org/fx v1.12.0
19 | go.uber.org/multierr v1.5.0 // indirect
20 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
21 | golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 // indirect
22 | gopkg.in/yaml.v2 v2.2.8
23 | honnef.co/go/tools v0.0.1-2020.1.3 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go/hasher/hasher.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | )
7 |
8 | // GetHash calculates an MD5 hash.
9 | func GetHash(data []byte) string {
10 | hash := md5.New()
11 | hash.Write(data)
12 | return hex.EncodeToString(hash.Sum(nil))
13 | }
14 |
--------------------------------------------------------------------------------
/go/linux_dsm_build.sh:
--------------------------------------------------------------------------------
1 | docker run -v "$PWD":/go/src/github.com/hednowley/sound -w /go/src/github.com/hednowley/sound golang:1.14-stretch /bin/bash -c "apt-get update; apt-get install build-essential gcc-6-arm-linux-gnueabihf -y; go get -d -v; GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc-6 go build"
--------------------------------------------------------------------------------
/go/log-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/go/migrations/000001_initial.down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/migrations/000001_initial.down.sql
--------------------------------------------------------------------------------
/go/migrations/000002_add_playlist_owner.down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/migrations/000002_add_playlist_owner.down.sql
--------------------------------------------------------------------------------
/go/migrations/000002_add_playlist_owner.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "playlists" ADD COLUMN "owner" TEXT NOT NULL DEFAULT '';
2 | ALTER TABLE "playlists" ALTER COLUMN "owner" DROP DEFAULT;
--------------------------------------------------------------------------------
/go/projectpath/Root.go:
--------------------------------------------------------------------------------
1 | package projectpath
2 |
3 | import (
4 | "path/filepath"
5 | "runtime"
6 | )
7 |
8 | var (
9 | _, b, _, _ = runtime.Caller(0)
10 |
11 | // Root folder of this project
12 | Root = filepath.Join(filepath.Dir(b), "..")
13 | )
14 |
--------------------------------------------------------------------------------
/go/provider/beetsProvider_test.go:
--------------------------------------------------------------------------------
1 | package provider_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/hednowley/sound/provider"
8 | )
9 |
10 | func Test1(t *testing.T) {
11 |
12 | p, err := provider.NewBeetsProvider("beets", "../testdata/beetslib.blb")
13 | if err != nil {
14 | t.Errorf("Could not make provider: %v", err)
15 | }
16 |
17 | p.Iterate(func(path string) error {
18 | fmt.Println(path)
19 | i, err := p.GetInfo(path)
20 | if err != nil {
21 | t.Errorf("Error getting info for %v: %v", path, err)
22 | }
23 | fmt.Println(i.Title)
24 |
25 | return nil
26 | })
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/go/provider/mockProvider.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/hednowley/sound/entities"
7 | )
8 |
9 | type MockProvider struct {
10 | files []*entities.FileInfo
11 | id string
12 | isScanning bool
13 | count int
14 | }
15 |
16 | func NewMockProvider(id string, files []*entities.FileInfo) *MockProvider {
17 | return &MockProvider{
18 | files: files,
19 | id: id,
20 | isScanning: false,
21 | count: 0,
22 | }
23 | }
24 |
25 | func (p *MockProvider) Iterate(callback func(path string) error) error {
26 | p.isScanning = true
27 | p.count = 0
28 |
29 | var callbackErr error
30 | for _, f := range p.files {
31 | callbackErr = callback(f.Path)
32 | p.count = p.count + 1
33 |
34 | if callbackErr != nil {
35 | break
36 | }
37 | }
38 | p.isScanning = false
39 | return callbackErr
40 | }
41 |
42 | func (p *MockProvider) GetInfo(path string) (*entities.FileInfo, error) {
43 | for _, f := range p.files {
44 | if f.Path == path {
45 | return f, nil
46 | }
47 | }
48 | return nil, errors.New("Bad path")
49 | }
50 |
51 | func (p *MockProvider) ID() string {
52 | return p.id
53 | }
54 |
55 | func (p *MockProvider) IsScanning() bool {
56 | return p.isScanning
57 | }
58 |
59 | func (p *MockProvider) FileCount() int64 {
60 | return int64(p.count)
61 | }
62 |
--------------------------------------------------------------------------------
/go/services/authenticator_test.go:
--------------------------------------------------------------------------------
1 | package services_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hednowley/sound/config"
7 | "github.com/hednowley/sound/services"
8 | )
9 |
10 | var c = config.Config{
11 | Users: []config.User{
12 | {
13 | Username: "billy",
14 | Password: "apple tart!!!",
15 | Email: "billy@bigbugs.com",
16 | },
17 | {
18 | Username: "tom tom",
19 | Password: "sfjksdfjk",
20 | Email: "tom@bigbugs.com",
21 | },
22 | },
23 | }
24 |
25 | var a = services.NewAuthenticator(&c)
26 |
27 | func TestPasswordAuth(t *testing.T) {
28 |
29 | // Should work
30 | if a.AuthenticateFromPassword("billy", "apple tart!!!") == nil {
31 | t.Error()
32 | }
33 |
34 | // Shouldn't work
35 | if a.AuthenticateFromPassword("billy2", "apple tart!!!") != nil {
36 | t.Error()
37 | }
38 |
39 | if a.AuthenticateFromPassword("billy", "appletart!!!") != nil {
40 | t.Error()
41 | }
42 |
43 | if a.AuthenticateFromPassword("", "") != nil {
44 | t.Error()
45 | }
46 | }
47 |
48 | func TestTokenAuth(t *testing.T) {
49 |
50 | // Should work
51 | if a.AuthenticateFromToken("tom tom", "saltysalt", "bbf729f2464585f1212e03519659b30a") == nil {
52 | t.Error()
53 | }
54 |
55 | // Shouldn't work
56 | if a.AuthenticateFromToken("tom tom", "saltysalt", "bbf729f2464585f12e03519659b30a") != nil {
57 | t.Error()
58 | }
59 |
60 | if a.AuthenticateFromToken("tom tom", "rocksaltt", "bbf729f2464585f1212e03519659b30a") != nil {
61 | t.Error()
62 | }
63 |
64 | if a.AuthenticateFromToken("", "", "") != nil {
65 | t.Error()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/go/services/clock.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Clock interface {
8 | GetTime() time.Time
9 | }
10 |
11 | type RealClock struct{}
12 |
13 | func (c *RealClock) GetTime() time.Time {
14 | return time.Now()
15 | }
16 |
17 | type MockClock struct{}
18 |
--------------------------------------------------------------------------------
/go/services/fileScanner.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strings"
7 | )
8 |
9 | func hasExtension(name string, extensions []string) bool {
10 | for _, e := range extensions {
11 | if strings.HasSuffix(name, "."+e) {
12 | return true
13 | }
14 | }
15 | return false
16 | }
17 |
18 | func IterateFiles(root string, extensions []string, action func(path string, info *os.FileInfo) error) (err error) {
19 |
20 | err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
21 | if !info.IsDir() && hasExtension(info.Name(), extensions) {
22 | return action(path, &info)
23 | }
24 | return nil
25 | })
26 | if err != nil {
27 | return
28 | }
29 |
30 | return
31 | }
32 |
--------------------------------------------------------------------------------
/go/services/fileScanner_test.go:
--------------------------------------------------------------------------------
1 | package services_test
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/hednowley/sound/services"
11 | )
12 |
13 | func TestScanner(t *testing.T) {
14 |
15 | errors := []string{}
16 | files := make(map[string]*bool)
17 |
18 | b1 := false
19 | b2 := false
20 | b3 := false
21 |
22 | files[filepath.Join("..", "testdata", "music", "1.mp3")] = &b1
23 | files[filepath.Join("..", "testdata", "music", "2.mp3")] = &b2
24 | files[filepath.Join("..", "testdata", "music", "subfolder", "3.mp3")] = &b3
25 |
26 | e := []string{
27 | "mp3",
28 | "flac",
29 | }
30 |
31 | path := filepath.Join("..", "testdata", "music")
32 | services.IterateFiles(path, e, func(path string, info *os.FileInfo) error {
33 | for k, v := range files {
34 | if strings.HasSuffix(path, k) {
35 | if *v {
36 | errors = append(errors, fmt.Sprintf("Double scan: %v", k))
37 | return nil
38 | }
39 | *files[k] = true
40 | return nil
41 | }
42 | }
43 |
44 | errors = append(errors, fmt.Sprintf("Unexpected scan: %v", path))
45 |
46 | return nil
47 | })
48 |
49 | if len(errors) > 0 {
50 | for _, v := range errors {
51 | t.Error(v)
52 | }
53 | }
54 |
55 | for k, v := range files {
56 | if !*v {
57 | t.Error(fmt.Sprintf("Unscanned file: %v", k))
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/go/services/resizer.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "image/jpeg"
5 | "image/png"
6 | "os"
7 |
8 | "github.com/nfnt/resize"
9 | )
10 |
11 | func Resize(originalPath string, newPath string, size uint) (err error) {
12 |
13 | file, err := os.Open(originalPath)
14 | if err != nil {
15 | return err
16 | }
17 | defer file.Close()
18 |
19 | img, err := jpeg.Decode(file)
20 | if err != nil {
21 | img, err = png.Decode(file)
22 | if err != nil {
23 | return err
24 | }
25 | }
26 |
27 | m := resize.Thumbnail(size, size, img, resize.NearestNeighbor)
28 |
29 | out, err := os.Create(newPath)
30 | if err != nil {
31 | return err
32 | }
33 | defer out.Close()
34 |
35 | return jpeg.Encode(out, m, nil)
36 | }
37 |
--------------------------------------------------------------------------------
/go/services/tagger.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/dhowden/tag"
7 | "github.com/hednowley/sound/entities"
8 | )
9 |
10 | // GetMusicData extracts data from a music file's ID3 tags.
11 | func GetMusicData(filePath string) (*entities.FileInfo, error) {
12 | file, _ := os.Open(filePath)
13 | defer file.Close()
14 |
15 | info, err := file.Stat()
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | m, _ := tag.ReadFrom(file)
21 |
22 | track, _ := m.Track()
23 | disc, _ := m.Disc()
24 |
25 | pic := m.Picture()
26 | var art *entities.CoverArtData
27 | if pic != nil {
28 | art = &entities.CoverArtData{
29 | Extension: pic.Ext,
30 | Raw: pic.Data,
31 | }
32 | }
33 |
34 | var albumArtist string
35 | if len(m.AlbumArtist()) == 0 {
36 | albumArtist = m.Artist()
37 | } else {
38 | albumArtist = m.AlbumArtist()
39 | }
40 |
41 | return &entities.FileInfo{
42 | Path: filePath,
43 | Artist: m.Artist(),
44 | Album: m.Album(),
45 | AlbumArtist: albumArtist,
46 | Title: m.Title(),
47 | Genre: m.Genre(),
48 | Year: m.Year(),
49 | Track: track,
50 | Disc: disc,
51 | CoverArt: art,
52 | Size: info.Size(),
53 | }, nil
54 | }
55 |
--------------------------------------------------------------------------------
/go/services/tagger_test.go:
--------------------------------------------------------------------------------
1 | package services_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hednowley/sound/services"
7 | )
8 |
9 | func TestTagReader(t *testing.T) {
10 | d, err := services.GetMusicData("../testdata/music/1.mp3")
11 | if err != nil {
12 | t.Error(err.Error())
13 | }
14 |
15 | if d.Title != "Front Street (Instrumental)" {
16 | t.Error()
17 | }
18 |
19 | if d.Album != "A Day Wit The Homiez (CD, 2002, RonnieCash.com)" {
20 | t.Error()
21 | }
22 |
23 | if d.AlbumArtist != "1st Down" {
24 | t.Error()
25 | }
26 |
27 | if d.Artist != "1st Down" {
28 | t.Error()
29 | }
30 |
31 | if d.Year != 1995 {
32 | t.Error()
33 | }
34 |
35 | if d.Track != 4 {
36 | t.Error()
37 | }
38 |
39 | if d.Genre != "Hip-Hop" {
40 | t.Error()
41 | }
42 |
43 | if d.Size != 8973172 {
44 | t.Error()
45 | }
46 |
47 | if d.Disc != 0 {
48 | t.Error()
49 | }
50 | }
51 |
52 | func TestReadMissingFile(t *testing.T) {
53 | _, err := services.GetMusicData("../dsaiuhsduihsd/1.mp3")
54 | if err == nil {
55 | t.Error(err.Error())
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/go/socket/connection.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 |
7 | "github.com/cihub/seelog"
8 | "github.com/gorilla/websocket"
9 | "github.com/hednowley/sound/socket/dto"
10 | )
11 |
12 | // Connection is a wrapper around a websocket connection.
13 | type Connection struct {
14 | Inner *websocket.Conn
15 | }
16 |
17 | // NewConnection creates a new connection.
18 | func NewConnection(inner *websocket.Conn) *Connection {
19 | return &Connection{
20 | Inner: inner,
21 | }
22 | }
23 |
24 | // SendMessage sends the response to the remote client.
25 | func (c *Connection) SendMessage(r *dto.Response) {
26 |
27 | body, err := json.Marshal(r)
28 | if err != nil {
29 |
30 | }
31 |
32 | c.Inner.WriteMessage(websocket.TextMessage, body)
33 | }
34 |
35 | // ReadMessage returns the last message sent from the remote client.
36 | func (c *Connection) ReadMessage() (*dto.Request, error) {
37 | messageType, payload, err := c.Inner.ReadMessage()
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | if messageType != websocket.TextMessage {
43 | return nil, errors.New("Non-text message received")
44 | }
45 |
46 | var r dto.Request
47 | err = json.Unmarshal(payload, &r)
48 | if err != nil {
49 | seelog.Errorf("Unexpected request: %v", string(payload))
50 | return nil, err
51 | }
52 |
53 | return &r, nil
54 | }
55 |
56 | // Close closes the connection.
57 | func (c *Connection) Close() error {
58 | return c.Inner.Close()
59 | }
60 |
--------------------------------------------------------------------------------
/go/socket/dto/album.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type Album struct {
10 | ID uint `json:"id"`
11 | Name string `json:"name"`
12 | Artist string `json:"artist"`
13 | ArtistID uint `json:"artistId,string"`
14 | Art string `json:"coverArt,omitempty"`
15 | Created *time.Time `json:"created"`
16 | Year int `json:"year,omitempty"`
17 | Genre string `json:"genre,omitempty"`
18 | Songs []*SongSummary `json:"songs"`
19 | Duration int `json:"duration"`
20 | }
21 |
22 | func NewAlbum(album *dao.Album, songs []dao.Song) *Album {
23 |
24 | songSummaries := make([]*SongSummary, len(songs))
25 | for index, song := range songs {
26 | songSummaries[index] = NewSongSummary(&song)
27 | }
28 |
29 | return &Album{
30 | Name: album.Name,
31 | ID: album.ID,
32 | ArtistID: album.ArtistID,
33 | Artist: album.ArtistName,
34 | Art: album.GetArt(),
35 | Created: album.Created,
36 | Genre: album.GetGenre(),
37 | Year: album.GetYear(),
38 | Songs: songSummaries,
39 | Duration: album.Duration,
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/go/socket/dto/albumCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import "github.com/hednowley/sound/dao"
4 |
5 | type AlbumCollection struct {
6 | Albums []*AlbumSummary `json:"albums"`
7 | }
8 |
9 | func NewAlbumCollection(albums []dao.Album) *AlbumCollection {
10 |
11 | dtoAlbums := make([]*AlbumSummary, len(albums))
12 | for index, a := range albums {
13 | dtoAlbums[index] = NewAlbumSummary(&a)
14 | }
15 |
16 | return &AlbumCollection{
17 | Albums: dtoAlbums,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/go/socket/dto/albumSummary.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type AlbumSummary struct {
10 | ID uint `json:"id"`
11 | Name string `json:"name"`
12 | Artist string `json:"artist"`
13 | ArtistID uint `json:"artistId,string"`
14 | Art string `json:"coverArt,omitempty"`
15 | Created *time.Time `json:"created"`
16 | Year int `json:"year,omitempty"`
17 | Genre string `json:"genre,omitempty"`
18 | Duration int `json:"duration"`
19 | }
20 |
21 | func NewAlbumSummary(album *dao.Album) *AlbumSummary {
22 | return &AlbumSummary{
23 | Name: album.Name,
24 | ID: album.ID,
25 | ArtistID: album.ArtistID,
26 | Artist: album.ArtistName,
27 | Art: album.GetArt(),
28 | Created: album.Created,
29 | Genre: album.GetGenre(),
30 | Year: album.GetYear(),
31 | Duration: album.Duration,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/go/socket/dto/artist.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "github.com/hednowley/sound/dao"
5 | )
6 |
7 | type Artist struct {
8 | ID uint `json:"id"`
9 | Name string `json:"name"`
10 | Art string `json:"coverArt,omitempty"`
11 | Albums []*AlbumSummary `json:"albums"`
12 | }
13 |
14 | func NewArtist(artist *dao.Artist, albums []dao.Album) *Artist {
15 |
16 | albumsDto := make([]*AlbumSummary, len(albums))
17 | for index, album := range albums {
18 | albumsDto[index] = NewAlbumSummary(&album)
19 | }
20 |
21 | return &Artist{
22 | ID: artist.ID,
23 | Name: artist.Name,
24 | Albums: albumsDto,
25 | Art: artist.GetArt(),
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/go/socket/dto/artistCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import "github.com/hednowley/sound/dao"
4 |
5 | type ArtistCollection struct {
6 | Artists []*ArtistSummary `json:"artists"`
7 | }
8 |
9 | type ArtistSummary struct {
10 | ID uint `json:"id"`
11 | Name string `json:"name"`
12 | }
13 |
14 | func NewArtistSummary(artist *dao.Artist) *ArtistSummary {
15 | return &ArtistSummary{
16 | ID: artist.ID,
17 | Name: artist.Name,
18 | }
19 | }
20 |
21 | func NewArtistCollection(artists []dao.Artist) *ArtistCollection {
22 |
23 | dtoArtists := make([]*ArtistSummary, len(artists))
24 | for index, a := range artists {
25 | dtoArtists[index] = NewArtistSummary(&a)
26 | }
27 |
28 | return &ArtistCollection{
29 | Artists: dtoArtists,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/go/socket/dto/notification.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | type Notification struct {
4 | Method string `json:"method"`
5 | Params map[string]interface{} `json:"params"`
6 | Version string `json:"jsonrpc"`
7 | }
8 |
9 | func NewNotification(method string, params map[string]interface{}) *Notification {
10 | return &Notification{
11 | Method: method,
12 | Params: params,
13 | Version: "2.0",
14 | }
15 | }
16 |
17 | func NewScanStatusNotification(scanning bool, count int64) *Notification {
18 | return NewNotification("scanStatus", map[string]interface{}{
19 | "scanning": scanning,
20 | "count": count,
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/go/socket/dto/playlist.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "github.com/hednowley/sound/dao"
5 | )
6 |
7 | type Playlist struct {
8 | ID uint `json:"id"`
9 | Name string `json:"name"`
10 | Songs []*SongSummary `json:"songs"`
11 | }
12 |
13 | func NewPlaylist(playlist *dao.Playlist, playlistSongs []dao.Song) *Playlist {
14 |
15 | songs := make([]*SongSummary, len(playlistSongs))
16 | for index, song := range playlistSongs {
17 | songs[index] = NewSongSummary(&song)
18 | }
19 |
20 | return &Playlist{
21 | Name: playlist.Name,
22 | ID: playlist.ID,
23 | Songs: songs,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/go/socket/dto/playlistCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "github.com/hednowley/sound/dao"
5 | )
6 |
7 | type playlistSummary struct {
8 | ID uint `json:"id"`
9 | Name string `json:"name"`
10 | }
11 |
12 | func newPlaylistSummary(playlist *dao.Playlist) *playlistSummary {
13 | return &playlistSummary{
14 | Name: playlist.Name,
15 | ID: playlist.ID,
16 | }
17 | }
18 |
19 | type PlaylistCollection struct {
20 | Playlists []*playlistSummary `json:"playlists"`
21 | }
22 |
23 | func NewPlaylistCollection(playlists []dao.Playlist) *PlaylistCollection {
24 | list := make([]*playlistSummary, len(playlists))
25 | for index, p := range playlists {
26 | list[index] = newPlaylistSummary(&p)
27 | }
28 |
29 | return &PlaylistCollection{
30 | Playlists: list,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/go/socket/dto/request.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import "encoding/json"
4 |
5 | type Request struct {
6 | Method string `json:"method"`
7 | Params map[string]*json.RawMessage `json:"params"`
8 | ID int `json:"id"`
9 | Version string `json:"jsonrpc"`
10 | }
11 |
--------------------------------------------------------------------------------
/go/socket/dto/response.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | const version = "2.0"
4 |
5 | // Response follows JSON-RPC syntax.
6 | type Response struct {
7 | Error string `json:"error,omitempty"`
8 | Result interface{} `json:"result,omitempty"`
9 | ID int `json:"id,omitempty"`
10 | Version string `json:"jsonrpc"`
11 | }
12 |
13 | func NewErrorResponse(message string, id int) *Response {
14 | return &Response{
15 | Error: message,
16 | ID: id,
17 | Version: version,
18 | }
19 | }
20 |
21 | func NewErrorNotification(message string) *Response {
22 | return &Response{
23 | Error: message,
24 | Version: version,
25 | }
26 | }
27 |
28 | func NewResponse(result interface{}, id int) *Response {
29 | return &Response{
30 | Result: result,
31 | ID: id,
32 | Version: version,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/go/socket/dto/songSummary.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "github.com/hednowley/sound/dao"
5 | )
6 |
7 | type SongSummary struct {
8 | ID uint `json:"id"`
9 | Name string `json:"name"`
10 | Track int `json:"track"`
11 | }
12 |
13 | func NewSongSummary(song *dao.Song) *SongSummary {
14 | return &SongSummary{
15 | Name: song.Title,
16 | ID: song.ID,
17 | Track: song.Track,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/go/socket/dto/ticketResponse.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | type TicketResponse struct {
4 | Accepted bool `json:"accepted"`
5 | }
6 |
--------------------------------------------------------------------------------
/go/socket/handler.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "github.com/hednowley/sound/config"
5 | "github.com/hednowley/sound/socket/dto"
6 | )
7 |
8 | type HandlerContext struct {
9 | User *config.User
10 | }
11 |
12 | // socket.Handler listens for particular websocket messages and
13 | // returns an object which will be sent back to the sender.
14 | type Handler = func(*dto.Request, *HandlerContext) interface{}
15 |
--------------------------------------------------------------------------------
/go/socket/handlers/getAlbum.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/socket"
9 | "github.com/hednowley/sound/socket/dto"
10 | )
11 |
12 | func MakeGetAlbumHandler(dal *dal.DAL) socket.Handler {
13 | return func(request *dto.Request, _ *socket.HandlerContext) interface{} {
14 | var id uint
15 |
16 | if request.Params["id"] == nil || json.Unmarshal(*request.Params["id"], &id) != nil {
17 | return "bad id"
18 | }
19 |
20 | conn, err := dal.Db.GetConn()
21 | if err != nil {
22 | return api.NewErrorReponse("Cannot make DB conn")
23 | }
24 | defer conn.Release()
25 |
26 | album, err := dal.Db.GetAlbum(conn, id)
27 | if err != nil {
28 | return "no album"
29 | }
30 |
31 | songs, err := dal.Db.GetAlbumSongs(conn, id)
32 | if err != nil {
33 | return api.NewErrorReponse("Error")
34 | }
35 |
36 | return dto.NewAlbum(album, songs)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/go/socket/handlers/getAlbums.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "github.com/hednowley/sound/api/api"
5 | "github.com/hednowley/sound/dal"
6 | "github.com/hednowley/sound/dao"
7 | "github.com/hednowley/sound/socket"
8 | "github.com/hednowley/sound/socket/dto"
9 | )
10 |
11 | func MakeGetAlbumsHandler(dal *dal.DAL) socket.Handler {
12 | return func(request *dto.Request, _ *socket.HandlerContext) interface{} {
13 | conn, err := dal.Db.GetConn()
14 | if err != nil {
15 | return api.NewErrorReponse("Cannot make DB conn")
16 | }
17 | defer conn.Release()
18 |
19 | albums, err := dal.Db.GetAlbums(conn, dao.AlphabeticalByName, 9999999, 0)
20 | if err != nil {
21 | return dto.NewErrorResponse("error", 0)
22 | }
23 | return dto.NewAlbumCollection(albums)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/go/socket/handlers/getArtist.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/socket"
9 | "github.com/hednowley/sound/socket/dto"
10 | )
11 |
12 | func MakeGetArtistHandler(dal *dal.DAL) socket.Handler {
13 | return func(request *dto.Request, _ *socket.HandlerContext) interface{} {
14 | var id uint
15 |
16 | if request.Params["id"] == nil || json.Unmarshal(*request.Params["id"], &id) != nil {
17 | return "bad id"
18 | }
19 |
20 | conn, err := dal.Db.GetConn()
21 | if err != nil {
22 | return api.NewErrorReponse("Cannot make DB conn")
23 | }
24 | defer conn.Release()
25 |
26 | artist, err := dal.Db.GetArtist(conn, id)
27 | if err != nil {
28 | return "no artist"
29 | }
30 |
31 | albums, err := dal.Db.GetAlbumsByArtist(conn, id)
32 | if err != nil {
33 | return api.NewErrorReponse("Error")
34 | }
35 |
36 | return dto.NewArtist(artist, albums)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/go/socket/handlers/getArtists.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "github.com/hednowley/sound/api/api"
5 | "github.com/hednowley/sound/dal"
6 | "github.com/hednowley/sound/socket"
7 | "github.com/hednowley/sound/socket/dto"
8 | )
9 |
10 | func MakeGetArtistsHandler(dal *dal.DAL) socket.Handler {
11 | return func(request *dto.Request, _ *socket.HandlerContext) interface{} {
12 | conn, err := dal.Db.GetConn()
13 | if err != nil {
14 | return api.NewErrorReponse("Cannot make DB conn")
15 | }
16 | defer conn.Release()
17 |
18 | artists, err := dal.Db.GetArtists(conn)
19 | if err != nil {
20 | return dto.NewErrorResponse("error", 0)
21 | }
22 | return dto.NewArtistCollection(artists)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/go/socket/handlers/getPlaylist.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/hednowley/sound/api/api"
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/socket"
9 | "github.com/hednowley/sound/socket/dto"
10 | )
11 |
12 | func MakeGetPlaylistHandler(dal *dal.DAL) socket.Handler {
13 | return func(request *dto.Request, context *socket.HandlerContext) interface{} {
14 | var id uint
15 |
16 | if request.Params["id"] == nil || json.Unmarshal(*request.Params["id"], &id) != nil {
17 | return "bad id"
18 | }
19 |
20 | conn, err := dal.Db.GetConn()
21 | if err != nil {
22 | return api.NewErrorReponse("Cannot make DB conn")
23 | }
24 | defer conn.Release()
25 |
26 | playlist, err := dal.Db.GetPlaylist(conn, id, context.User.Username)
27 | if err != nil {
28 | return "no playlist"
29 | }
30 |
31 | songs, err := dal.Db.GetPlaylistSongs(conn, id, context.User.Username)
32 | if err != nil {
33 | return "no playlist"
34 | }
35 |
36 | return dto.NewPlaylist(playlist, songs)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/go/socket/handlers/getPlaylists.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "github.com/hednowley/sound/api/api"
5 | "github.com/hednowley/sound/dal"
6 | "github.com/hednowley/sound/socket"
7 | "github.com/hednowley/sound/socket/dto"
8 | )
9 |
10 | func MakeGetPlaylistsHandler(dal *dal.DAL) socket.Handler {
11 | return func(request *dto.Request, context *socket.HandlerContext) interface{} {
12 | conn, err := dal.Db.GetConn()
13 | if err != nil {
14 | return api.NewErrorReponse("Cannot make DB conn")
15 | }
16 | defer conn.Release()
17 |
18 | playlists, err := dal.Db.GetPlaylists(conn, context.User.Username)
19 | if err != nil {
20 | return api.NewErrorReponse("Error")
21 | }
22 |
23 | return dto.NewPlaylistCollection(playlists)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/go/socket/handlers/startScan.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/hednowley/sound/provider"
7 | "github.com/hednowley/sound/socket"
8 | "github.com/hednowley/sound/socket/dto"
9 | )
10 |
11 | func MakeStartScanHandler(scanner *provider.Scanner) socket.Handler {
12 | return func(request *dto.Request, _ *socket.HandlerContext) interface{} {
13 | var update bool
14 | var delete bool
15 |
16 | if request.Params["update"] == nil || json.Unmarshal(*request.Params["update"], &update) != nil {
17 | update = false
18 | }
19 |
20 | if request.Params["delete"] == nil || json.Unmarshal(*request.Params["delete"], &delete) != nil {
21 | delete = false
22 | }
23 |
24 | go scanner.StartAllScans(update, delete)
25 | return struct{}{}
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/go/socket/mockHub.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/hednowley/sound/socket/dto"
7 | )
8 |
9 | type MockHub struct {
10 | }
11 |
12 | func NewMockHub() IHub {
13 | return &MockHub{}
14 | }
15 |
16 | func (h *MockHub) SetHandler(method string, handler Handler) {
17 | }
18 |
19 | // Run starts the hub.
20 | func (h *MockHub) Run() {
21 | }
22 |
23 | // Notify sends a notification to all clients.
24 | func (h *MockHub) Notify(notification *dto.Notification) {
25 | }
26 |
27 | // Notify sends a notification to all clients.
28 | func (h *MockHub) AddClient(w http.ResponseWriter, r *http.Request) {
29 | }
30 |
--------------------------------------------------------------------------------
/go/socket/ticketer.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | "github.com/hednowley/sound/config"
7 | "time"
8 | )
9 |
10 | // Ticket allows a user to negotiate a websocket session.
11 | type Ticket struct {
12 | user *config.User
13 | expires time.Time
14 | }
15 |
16 | func (t *Ticket) hasExpired() bool {
17 | return t.expires.Before(time.Now())
18 | }
19 |
20 | // Ticketer creates and monitors tickets.
21 | type Ticketer struct {
22 | // How long after its creation a ticket expires
23 | duration time.Duration
24 | tickets map[string]Ticket
25 | }
26 |
27 | // NewTicketer creates a new ticketer.
28 | func NewTicketer(config *config.Config) *Ticketer {
29 | return &Ticketer{
30 | duration: time.Second * time.Duration(config.WebsocketTicketExpiry),
31 | tickets: make(map[string]Ticket),
32 | }
33 | }
34 |
35 | // MakeTicket creates a new ticket.
36 | func (t *Ticketer) MakeTicket(user *config.User) string {
37 | t.cleanTickets()
38 |
39 | b := make([]byte, 20)
40 | rand.Read(b)
41 | s := base64.URLEncoding.EncodeToString(b)
42 |
43 | t.tickets[s] = Ticket{
44 | expires: time.Now().Add(t.duration),
45 | user: user,
46 | }
47 |
48 | return s
49 | }
50 |
51 | // SubmitTicket sees if there is a ticket with the given key.
52 | // If there is, then the user which created the ticket is returned.
53 | // Otherwise returns nil.
54 | func (t *Ticketer) SubmitTicket(key string) *config.User {
55 | if ticket, ok := t.tickets[key]; ok {
56 | // Delete so ticket can't be used twice
57 | delete(t.tickets, key)
58 | return ticket.user
59 | }
60 |
61 | return nil
62 | }
63 |
64 | // Removes all expired tickets.
65 | func (t *Ticketer) cleanTickets() {
66 | for value, ticket := range t.tickets {
67 | if ticket.hasExpired() {
68 | delete(t.tickets, value)
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/go/sound.conf:
--------------------------------------------------------------------------------
1 | start on syno.share.ready and syno.network.ready and started pgsql-adapter
2 | stop on runlevel [06]
3 |
4 | chdir /volume1/other/soundDir
5 | exec ./sound
6 |
--------------------------------------------------------------------------------
/go/subsonic/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 |
7 | "github.com/hednowley/sound/config"
8 | )
9 |
10 | var version = "1.16.1"
11 |
12 | type HandlerContext struct {
13 | User *config.User
14 | }
15 |
16 | // Handler is a web controller action.
17 | // It accepts a set of parameters and returns an unserialised Response.
18 | type Handler func(url.Values, *HandlerContext) *Response
19 |
20 | // BinaryHandler is a low-level web controller action.
21 | // It accepts a set of parameters, a ResponseWriter and a Request.
22 | // It returns a nil pointer to indicate that no response is needed,
23 | // otherwise it returns an unserialised Response.
24 | type BinaryHandler func(url.Values, *http.ResponseWriter, *http.Request, *HandlerContext) *Response
25 |
--------------------------------------------------------------------------------
/go/subsonic/api/format.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | type responseFormat int
4 |
5 | const (
6 | jsonFormat responseFormat = 0
7 | xmlFormat responseFormat = 1
8 | )
9 |
10 | var defaultFormat = xmlFormat
11 |
--------------------------------------------------------------------------------
/go/subsonic/api/parse.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/url"
5 | "strings"
6 | )
7 |
8 | // parseResponseFormat tries to parse a string into a response format.
9 | // If this is not possible then a nil pointer is returned instead.
10 | func parseResponseFormat(param string) *responseFormat {
11 | if len(param) == 0 {
12 | return &defaultFormat
13 | }
14 |
15 | param = strings.ToLower(param)
16 | if param == "json" {
17 | f := jsonFormat
18 | return &f
19 | }
20 |
21 | if param == "xml" {
22 | f := xmlFormat
23 | return &f
24 | }
25 |
26 | return nil
27 | }
28 |
29 | // parseParams extracts parameters from a Request.
30 | // It merges URL queries and the request body
31 | // (body wins where there are collisions)
32 | func parseParams(urlQuery url.Values, body []byte) url.Values {
33 |
34 | values := url.Values{}
35 |
36 | for k, v := range urlQuery {
37 | for _, s := range v {
38 | values.Add(k, s)
39 | }
40 | }
41 |
42 | bodyParams, err := url.ParseQuery(string(body))
43 | if err == nil {
44 | for k, v := range bodyParams {
45 | for _, s := range v {
46 | values.Add(k, s)
47 | }
48 | }
49 | }
50 |
51 | return values
52 | }
53 |
--------------------------------------------------------------------------------
/go/subsonic/api/response.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/hednowley/sound/subsonic/dto"
5 | )
6 |
7 | type Response struct {
8 | Body interface{}
9 | IsSuccess bool
10 | }
11 |
12 | func NewErrorReponse(code dto.ErrorCode, message string) *Response {
13 | return &Response{
14 | Body: dto.NewError(code, message),
15 | IsSuccess: false,
16 | }
17 | }
18 |
19 | func NewSuccessfulReponse(body interface{}) *Response {
20 | return &Response{
21 | Body: body,
22 | IsSuccess: true,
23 | }
24 | }
25 |
26 | func NewEmptyReponse() *Response {
27 | return &Response{
28 | Body: nil,
29 | IsSuccess: true,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/go/subsonic/api/serialiser.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "encoding/xml"
6 | "fmt"
7 | "reflect"
8 | )
9 |
10 | func getStatus(succeeded bool) string {
11 | if succeeded {
12 | return "ok"
13 | }
14 | return "failed"
15 | }
16 |
17 | func serialiseToJSON(response *Response) string {
18 | var body *string
19 | var name string
20 |
21 | if response.Body != nil {
22 | b, err := json.Marshal(response.Body)
23 | if err == nil {
24 | s := string(b)
25 | body = &s
26 |
27 | // Dereference response.Body if it is a pointer
28 | v := reflect.ValueOf(response.Body)
29 | if v.Kind() == reflect.Ptr {
30 | v = v.Elem()
31 | }
32 |
33 | nameField, _ := v.Type().FieldByName("XMLName")
34 | name = nameField.Tag.Get("xml")
35 | }
36 | }
37 |
38 | responseJSON, _ := json.Marshal(response.Body)
39 |
40 | status := getStatus(response.IsSuccess)
41 |
42 | if body == nil || len(name) == 0 {
43 | return fmt.Sprintf(`{"subsonic-response":{"status":"%v","version":"%v"}}`,
44 | status, version)
45 | }
46 |
47 | return fmt.Sprintf(
48 | `{"subsonic-response":{"status":"%v","version":"%v", "%v": %s}}`,
49 | status, version, name, responseJSON)
50 | }
51 |
52 | func serialiseToXML(response *Response) string {
53 | var body string
54 | if response.Body != nil {
55 | b, err := xml.Marshal(response.Body)
56 | if err == nil {
57 | body = string(b)
58 | }
59 | }
60 |
61 | return fmt.Sprintf(
62 | `%v`,
63 | getStatus(response.IsSuccess),
64 | version,
65 | body)
66 | }
67 |
--------------------------------------------------------------------------------
/go/subsonic/api/serialiser_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "testing"
7 | )
8 |
9 | func TestEmptySerialiser(t *testing.T) {
10 |
11 | r := Response{
12 | Body: nil,
13 | IsSuccess: true,
14 | }
15 |
16 | j := serialiseToJSON(&r)
17 | if j != fmt.Sprintf(`{"subsonic-response":{"status":"ok","version":"%v"}}`, version) {
18 | t.Error()
19 | }
20 |
21 | r.IsSuccess = false
22 |
23 | x := serialiseToXML(&r)
24 | if x != fmt.Sprintf(``, version) {
25 | t.Error()
26 | }
27 | }
28 |
29 | func TestSerialiser(t *testing.T) {
30 |
31 | type TestResponseDto struct {
32 | XMLName xml.Name `xml:"test-thing" json:"-"`
33 | ID uint `xml:"id,attr" json:"id,string"`
34 | Thingy string `xml:"thingy,attr" json:"thingy"`
35 | }
36 |
37 | r := Response{
38 | Body: &TestResponseDto{
39 | ID: 5,
40 | Thingy: "dyig",
41 | },
42 | IsSuccess: false,
43 | }
44 |
45 | j := serialiseToJSON(&r)
46 | if j != fmt.Sprintf(`{"subsonic-response":{"status":"failed","version":"%v", "test-thing": {"id":"5","thingy":"dyig"}}}`, version) {
47 | t.Error()
48 | }
49 |
50 | r.IsSuccess = true
51 |
52 | x := serialiseToXML(&r)
53 | if x != fmt.Sprintf(``, version) {
54 | t.Error()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/go/subsonic/dto/album.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 | "time"
6 |
7 | "github.com/hednowley/sound/dao"
8 | )
9 |
10 | type albumBody struct {
11 | ID uint `xml:"id,attr" json:"id,string"`
12 | Name string `xml:"name,attr" json:"name"`
13 | Artist string `xml:"artist,attr" json:"artist"`
14 | ArtistID uint `xml:"artistId,attr" json:"artistId,string"`
15 | Art string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
16 | SongCount uint `xml:"songCount,attr" json:"songCount"`
17 | Duration int `xml:"duration,attr" json:"duration"`
18 | Created *time.Time `xml:"created,attr" json:"created"`
19 | Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
20 | Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
21 | }
22 |
23 | func newAlbumBody(album *dao.Album) *albumBody {
24 |
25 | return &albumBody{
26 | ID: album.ID,
27 | Name: album.Name,
28 | ArtistID: album.ArtistID,
29 | SongCount: album.SongCount,
30 | Artist: album.ArtistName,
31 | Art: album.GetArt(),
32 | Created: album.Created,
33 | Genre: album.GetGenre(),
34 | Year: album.GetYear(),
35 | Duration: album.Duration,
36 | }
37 |
38 | }
39 |
40 | type Album struct {
41 | XMLName xml.Name `xml:"album" json:"-"`
42 | *albumBody
43 | }
44 |
45 | func NewAlbum(album *dao.Album) *Album {
46 | return &Album{
47 | XMLName: xml.Name{},
48 | albumBody: newAlbumBody(album),
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/go/subsonic/dto/albumDirectory.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type albumDirectoryBody struct {
10 | *Directory
11 | *albumBody
12 | }
13 |
14 | type AlbumDirectory struct {
15 | XMLName xml.Name `xml:"directory" json:"-"`
16 | *albumDirectoryBody
17 | Children []*SongChildDirectory `xml:"child" json:"child"`
18 | }
19 |
20 | type AlbumChildDirectory struct {
21 | XMLName xml.Name `xml:"child" json:"-"`
22 | *albumDirectoryBody
23 | }
24 |
25 | func newAlbumDirectoryBody(album *dao.Album) *albumDirectoryBody {
26 | return &albumDirectoryBody{
27 | Directory: &Directory{
28 | ID: NewAlbumID(album.ID),
29 | IsDir: true,
30 | Parent: NewArtistID(album.ArtistID),
31 | },
32 | albumBody: newAlbumBody(album),
33 | }
34 | }
35 |
36 | func NewAlbumDirectory(album *dao.Album, songs []dao.Song) *AlbumDirectory {
37 | children := make([]*SongChildDirectory, len(songs))
38 | for index, song := range songs {
39 | children[index] = NewSongChildDirectory(&song)
40 | }
41 |
42 | return &AlbumDirectory{xml.Name{}, newAlbumDirectoryBody(album), children}
43 | }
44 |
45 | func NewAlbumChildDirectory(album *dao.Album) *AlbumChildDirectory {
46 | return &AlbumChildDirectory{xml.Name{}, newAlbumDirectoryBody(album)}
47 | }
48 |
--------------------------------------------------------------------------------
/go/subsonic/dto/albumList2.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type AlbumList2 struct {
10 | XMLName xml.Name `xml:"albumList2" json:"-"`
11 | Albums []*Album `xml:"album" json:"album"`
12 | }
13 |
14 | func NewAlbumList2(albums []dao.Album) *AlbumList2 {
15 |
16 | dtoAlbums := make([]*Album, len(albums))
17 | for index, a := range albums {
18 | dtoAlbums[index] = NewAlbum(&a)
19 | }
20 |
21 | return &AlbumList2{Albums: dtoAlbums}
22 | }
23 |
--------------------------------------------------------------------------------
/go/subsonic/dto/albumWithSongs.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type albumWithSongsBody struct {
10 | Songs []*Song `xml:"song" json:"song,omitempty"`
11 | }
12 |
13 | func newAlbumWithSongsBody(album *dao.Album, songs []dao.Song) *albumWithSongsBody {
14 |
15 | songsDto := make([]*Song, len(songs))
16 | for index, song := range songs {
17 | songsDto[index] = NewSong(&song)
18 |
19 | }
20 |
21 | return &albumWithSongsBody{
22 |
23 | Songs: songsDto,
24 | }
25 |
26 | }
27 |
28 | type AlbumWithSongs struct {
29 | XMLName xml.Name `xml:"album" json:"-"`
30 | *albumBody
31 | *albumWithSongsBody
32 | }
33 |
34 | func NewAlbumWithSongs(album *dao.Album, songs []dao.Song) *AlbumWithSongs {
35 | return &AlbumWithSongs{
36 | albumBody: newAlbumBody(album),
37 | albumWithSongsBody: newAlbumWithSongsBody(album, songs),
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/go/subsonic/dto/artist.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type Artist struct {
10 | XMLName xml.Name `xml:"artist" json:"-"`
11 | ID uint `xml:"id,attr" json:"id,string"`
12 | Name string `xml:"name,attr" json:"name"`
13 | Art string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
14 | AlbumCount uint `xml:"albumCount,attr" json:"albumCount"`
15 | Duration int `xml:"duration,attr" json:"duration"`
16 | }
17 |
18 | func NewArtist(artist *dao.Artist) *Artist {
19 |
20 | return &Artist{
21 | ID: artist.ID,
22 | Name: artist.Name,
23 | AlbumCount: artist.AlbumCount,
24 | Art: artist.GetArt(),
25 | Duration: artist.Duration,
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/go/subsonic/dto/artistDirectory.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type ArtistDirectory struct {
10 | *ArtistDirectorySummary
11 | Children []*AlbumChildDirectory `xml:"child" json:"child"`
12 | }
13 |
14 | type ArtistDirectorySummary struct {
15 | XMLName xml.Name `xml:"directory" json:"-"`
16 | *Directory
17 | Name string `xml:"name,attr" json:"name"`
18 | }
19 |
20 | func NewArtistDirectorySummary(artist *dao.Artist) *ArtistDirectorySummary {
21 | return &ArtistDirectorySummary{
22 | XMLName: xml.Name{},
23 | Directory: &Directory{
24 | ID: NewArtistID(artist.ID),
25 | IsDir: true,
26 | },
27 | Name: artist.Name,
28 | }
29 | }
30 |
31 | func NewArtistDirectory(artist *dao.Artist, artistAlbums []dao.Album) *ArtistDirectory {
32 |
33 | albums := make([]*AlbumChildDirectory, len(artistAlbums))
34 | for i, a := range artistAlbums {
35 | albums[i] = NewAlbumChildDirectory(&a)
36 | }
37 |
38 | return &ArtistDirectory{
39 | NewArtistDirectorySummary(artist),
40 | albums,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/go/subsonic/dto/artistWithAlbums.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | // TODO: Extract intersection with Artist
10 | type ArtistWithAlbums struct {
11 | XMLName xml.Name `xml:"artist" json:"-"`
12 | ID uint `xml:"id,attr" json:"id,string"`
13 | Name string `xml:"name,attr" json:"name"`
14 | Art string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
15 | AlbumCount uint `xml:"albumCount,attr" json:"albumCount"`
16 | Albums []*Album `xml:"album" json:"album,omitempty"`
17 | Duration int `xml:"duration,attr" json:"duration"`
18 | }
19 |
20 | func NewArtistWithAlbums(artist *dao.Artist, albums []dao.Album) *ArtistWithAlbums {
21 |
22 | albumDTOs := make([]*Album, len(albums))
23 | for index, album := range albums {
24 | albumDTOs[index] = NewAlbum(&album)
25 | }
26 |
27 | return &ArtistWithAlbums{
28 | ID: artist.ID,
29 | Name: artist.Name,
30 | AlbumCount: artist.AlbumCount,
31 | Albums: albumDTOs,
32 | Art: artist.GetArt(),
33 | Duration: artist.Duration,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/go/subsonic/dto/directory.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | type Directory struct {
4 | ID string `xml:"id,attr" json:"id"`
5 | IsDir bool `xml:"isDir,attr" json:"isDir"`
6 | Parent string `xml:"parent,attr,omitempty" json:"parent,omitempty"`
7 | }
8 |
--------------------------------------------------------------------------------
/go/subsonic/dto/directoryID.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "github.com/hednowley/sound/util"
8 | )
9 |
10 | type DirectoryType int
11 |
12 | const (
13 | SongDirectoryType DirectoryType = 0
14 | AlbumDirectoryType DirectoryType = 1
15 | ArtistDirectoryType DirectoryType = 2
16 | albumPrefix = "album_"
17 | artistPrefix = "artist_"
18 | )
19 |
20 | type DirectoryID struct {
21 | ID uint
22 | Type DirectoryType
23 | }
24 |
25 | func ParseDirectoryID(s string) (*DirectoryID, error) {
26 | if strings.HasPrefix(s, albumPrefix) {
27 | id := util.ParseUint(strings.TrimPrefix(s, albumPrefix), 0)
28 | if id == 0 {
29 | return nil, errors.New("Bad album ID")
30 | }
31 | return &DirectoryID{id, AlbumDirectoryType}, nil
32 | }
33 |
34 | if strings.HasPrefix(s, artistPrefix) {
35 | id := util.ParseUint(strings.TrimPrefix(s, artistPrefix), 0)
36 | if id == 0 {
37 | return nil, errors.New("Bad artist ID")
38 | }
39 | return &DirectoryID{id, ArtistDirectoryType}, nil
40 | }
41 |
42 | id := util.ParseUint(s, 0)
43 | if id == 0 {
44 | return nil, errors.New("Unknown directory ID")
45 | }
46 | return &DirectoryID{id, SongDirectoryType}, nil
47 |
48 | }
49 |
50 | func NewSongID(id uint) string {
51 | return util.FormatUint(id)
52 | }
53 |
54 | func NewAlbumID(id uint) string {
55 | return albumPrefix + util.FormatUint(id)
56 | }
57 |
58 | func NewArtistID(id uint) string {
59 | return artistPrefix + util.FormatUint(id)
60 | }
61 |
--------------------------------------------------------------------------------
/go/subsonic/dto/error.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 | )
6 |
7 | type Error struct {
8 | XMLName xml.Name `xml:"error" json:"-"`
9 | Code int `xml:"code,attr" json:"code"`
10 | Message string `xml:"message,attr" json:"message"`
11 | }
12 |
13 | type ErrorCode int
14 |
15 | const (
16 | Generic ErrorCode = 0
17 | MissingParameter ErrorCode = 10
18 | ClientTooOld ErrorCode = 20
19 | ServerTooOld ErrorCode = 30
20 | WrongCredentials ErrorCode = 40
21 | TokenAuthUnsupported ErrorCode = 41
22 | NotAuthorised ErrorCode = 50
23 | BadLicense ErrorCode = 60
24 | NotFound ErrorCode = 70
25 | )
26 |
27 | func NewError(code ErrorCode, message string) Error {
28 |
29 | return Error{
30 | Code: int(code),
31 | Message: message,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/go/subsonic/dto/genres.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type Genres struct {
10 | XMLName xml.Name `xml:"genres" json:"-"`
11 | Genres []Genre `xml:"genre" json:"genre"`
12 | }
13 |
14 | type Genre struct {
15 | XMLName xml.Name `xml:"genre" json:"-"`
16 | SongCount int `xml:"songCount,attr" json:"songCount"`
17 | AlbumCount int `xml:"albumCount,attr" json:"albumCount"`
18 | Name string `xml:",chardata" json:"value"`
19 | }
20 |
21 | func NewGenre(genre *dao.Genre) *Genre {
22 | return &Genre{
23 | SongCount: genre.SongCount,
24 | AlbumCount: genre.AlbumCount,
25 | Name: genre.Name,
26 | }
27 | }
28 |
29 | func NewGenres(genres []dao.Genre) *Genres {
30 |
31 | count := len(genres)
32 | dtoGenres := make([]Genre, count)
33 |
34 | for i, g := range genres {
35 | dtoGenres[i] = *NewGenre(&g)
36 | }
37 |
38 | return &Genres{
39 | Genres: dtoGenres,
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/go/subsonic/dto/genres_test.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hednowley/sound/database"
7 | )
8 |
9 | func TestGenres(t *testing.T) {
10 |
11 | // genres := GenerateGenres(4)
12 | // art := GenerateArt(1)
13 | // artist := GenerateArtist(1, art)
14 | // albums1 := GenerateAlbums(5, &genres[0], artist, art)
15 | // GenerateAlbums(3, &genres[1], artist, art)
16 | // GenerateAlbums(1, &genres[2], artist, art)
17 |
18 | // GenerateSongs(1, &genres[0], &albums1[0], art)
19 | // GenerateSongs(2, &genres[1], &albums1[0], art)
20 | // GenerateSongs(3, &genres[2], &albums1[0], art)
21 |
22 | db := database.NewMock()
23 | conn, _ := db.GetConn()
24 | defer conn.Release()
25 |
26 | genres, err := db.GetGenres(conn)
27 | if err != nil {
28 | t.Error(err)
29 | }
30 |
31 | DTO := NewGenres(genres)
32 |
33 | xml := `
34 |
35 | genre_1
36 | genre_2
37 | genre_4
38 |
39 | `
40 |
41 | json := `
42 | {
43 | "genre":[
44 | {
45 | "songCount":3,
46 | "albumCount":3,
47 | "value":"genre_1"
48 | },
49 | {
50 | "songCount":1,
51 | "albumCount":1,
52 | "value":"genre_2"
53 | },
54 | {
55 | "songCount":0,
56 | "albumCount":0,
57 | "value":"genre_4"
58 | }
59 | ]
60 | }
61 | `
62 |
63 | err = CheckSerialisation(DTO, xml, json)
64 | if err != nil {
65 | t.Error(err.Error())
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/go/subsonic/dto/license.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 | )
6 |
7 | // License is a minimal Subsonic software license.
8 | type License struct {
9 | XMLName xml.Name `xml:"license" json:"-"`
10 | Valid bool `xml:"valid,attr" json:"valid"`
11 | }
12 |
13 | // NewLicense makes a new License.
14 | func NewLicense() *License {
15 | return &License{
16 | xml.Name{},
17 | true,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/go/subsonic/dto/musicFolderCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/provider"
7 | )
8 |
9 | type MusicFolderCollection struct {
10 | XMLName xml.Name `xml:"musicFolders" json:"-"`
11 | Folders []*musicFolder `xml:"musicFolder,attr" json:"musicFolder"`
12 | }
13 |
14 | type musicFolder struct {
15 | XMLName xml.Name `xml:"musicFolder" json:"-"`
16 | ID int `xml:"id,attr" json:"id"`
17 | Name string `xml:"name" json:"name"`
18 | }
19 |
20 | func NewMusicFolderCollection(providers []provider.Provider) *MusicFolderCollection {
21 |
22 | folders := make([]*musicFolder, len(providers))
23 | for i, p := range providers {
24 | folders[i] = &musicFolder{ID: i, Name: p.ID()}
25 | }
26 |
27 | return &MusicFolderCollection{Folders: folders}
28 | }
29 |
--------------------------------------------------------------------------------
/go/subsonic/dto/playlistCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type PlaylistCollection struct {
10 | XMLName xml.Name `xml:"playlists" json:"-"`
11 | Playlists []*PlaylistCollectionItem `xml:"playlist" json:"playlist"`
12 | }
13 |
14 | type PlaylistCollectionItem struct {
15 | XMLName xml.Name `xml:"playlist" json:"-"`
16 | *PlaylistCore
17 | }
18 |
19 | func NewPlaylistCollectionItem(playlist *dao.Playlist) *PlaylistCollectionItem {
20 | return &PlaylistCollectionItem{
21 | xml.Name{},
22 | newPlaylistCore(playlist),
23 | }
24 | }
25 |
26 | func NewPlaylistCollection(playlists []dao.Playlist) *PlaylistCollection {
27 |
28 | count := len(playlists)
29 | dtoCollection := make([]*PlaylistCollectionItem, count)
30 |
31 | for i, p := range playlists {
32 | dtoCollection[i] = NewPlaylistCollectionItem(&p)
33 | }
34 |
35 | return &PlaylistCollection{
36 | Playlists: dtoCollection,
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/go/subsonic/dto/playlistCollection_test.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | /*
4 | var playlistCollectionDTO = NewPlaylistCollection([]dao.Playlist{
5 | playlist1,
6 | playlist2,
7 | playlist3,
8 | })
9 |
10 | func TestPlaylistCollectionXml(t *testing.T) {
11 |
12 | body, err := xml.Marshal(playlistCollectionDTO)
13 | if err != nil {
14 | t.Error(err.Error())
15 | }
16 |
17 | if string(body) != `` {
18 | fmt.Println(string(body))
19 | t.Error("Bad serialisation")
20 | }
21 |
22 | }
23 |
24 | func TestPlaylistCollectionJson(t *testing.T) {
25 | body, err := json.Marshal(playlistCollectionDTO)
26 | if err != nil {
27 | t.Error(err.Error())
28 | }
29 |
30 | if string(body) != `{"playlist":[{"id":1,"songCount":3,"name":"fghgfd hfg","public":true,"created":"2014-11-12T11:45:26.371Z","changed":"2014-11-12T11:45:26.371Z","owner":"ned","comment":"hufdggdsh"},{"id":2,"songCount":1,"name":"iuafghfd dhsd","public":false,"created":"2014-11-12T11:45:26.371Z","changed":"2014-11-12T11:45:26.371Z","owner":"ned","comment":"fhfgh"},{"id":3,"songCount":0,"name":"fhgfh","public":true,"created":"2014-11-12T11:45:26.371Z","changed":"2014-11-12T11:45:26.371Z","owner":"ned","comment":"dgf"}]}` {
31 | fmt.Println(string(body))
32 | t.Error("Bad serialisation")
33 | }
34 |
35 | }
36 |
37 | */
38 |
--------------------------------------------------------------------------------
/go/subsonic/dto/randomSongs.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type RandomSongs struct {
10 | XMLName xml.Name `xml:"randomSongs" json:"-"`
11 | Songs []*Song `xml:"song" json:"song"`
12 | }
13 |
14 | func NewRandomSongs(songs []dao.Song) *RandomSongs {
15 |
16 | dto := make([]*Song, len(songs))
17 |
18 | for i, s := range songs {
19 | dto[i] = NewSong(&s)
20 | }
21 | return &RandomSongs{
22 | Songs: dto,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/go/subsonic/dto/randomSongs_test.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/json"
5 | "encoding/xml"
6 | "fmt"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | func TestRandomSongs(t *testing.T) {
12 |
13 | genre := GenerateGenre(1)
14 | art := GenerateArt(1)
15 | artist := GenerateArtist(1, art)
16 | album := GenerateAlbum(1, genre, artist, art)
17 | songs := GenerateSongs(5, genre, album, art)
18 |
19 | DTO := NewRandomSongs(songs)
20 |
21 | innerXML := ""
22 | for _, s := range songs {
23 | m, _ := xml.Marshal(NewSong(&s))
24 | innerXML += string(m)
25 | }
26 |
27 | xml := fmt.Sprintf(`
28 | %v
29 | `, innerXML)
30 |
31 | innerJSON := make([]string, 5)
32 | for i, s := range songs {
33 | m, _ := json.Marshal(NewSong(&s))
34 | innerJSON[i] = string(m)
35 | }
36 |
37 | json := fmt.Sprintf(`
38 | {
39 | "song":[%v]
40 | }
41 | `, strings.Join(innerJSON, ","))
42 |
43 | err := CheckSerialisation(DTO, xml, json)
44 | if err != nil {
45 | t.Error(err.Error())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/go/subsonic/dto/scanStatus.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 | )
6 |
7 | // ScanStatus describes the progress of all music scans.
8 | type ScanStatus struct {
9 | XMLName xml.Name `xml:"scanStatus" json:"-"`
10 | Scanning bool `xml:"scanning,attr" json:"scanning"`
11 | Count int64 `xml:"count,attr" json:"count"`
12 | }
13 |
14 | // NewScanStatus makes a new ScanStatus DTO.
15 | func NewScanStatus(scanning bool, count int64) ScanStatus {
16 | return ScanStatus{
17 | Scanning: scanning,
18 | Count: count,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/go/subsonic/dto/scanStatus_test.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestScanningStatus(t *testing.T) {
8 |
9 | DTO := NewScanStatus(true, 45485)
10 |
11 | xml := ``
12 |
13 | json := `
14 | {
15 | "scanning":true,
16 | "count":45485
17 | }
18 | `
19 |
20 | err := CheckSerialisation(DTO, xml, json)
21 | if err != nil {
22 | t.Error(err.Error())
23 | }
24 | }
25 |
26 | func TestNotScanningStatus(t *testing.T) {
27 |
28 | DTO := NewScanStatus(false, 0)
29 |
30 | xml := ``
31 |
32 | json := `
33 | {
34 | "scanning":false,
35 | "count":0
36 | }
37 | `
38 |
39 | err := CheckSerialisation(DTO, xml, json)
40 | if err != nil {
41 | t.Error(err.Error())
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/go/subsonic/dto/search.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type SearchCore struct {
10 | Artists []*Artist `xml:"artist" json:"artist"`
11 | Albums []*Album `xml:"album" json:"album"`
12 | Songs []*Song `xml:"song" json:"song"`
13 | }
14 |
15 | type Search2Response struct {
16 | XMLName xml.Name `xml:"searchResult2" json:"-"`
17 | *SearchCore
18 | }
19 |
20 | type Search3Response struct {
21 | XMLName xml.Name `xml:"searchResult3" json:"-"`
22 | *SearchCore
23 | }
24 |
25 | func newSearchResponse(artists []dao.Artist, albums []dao.Album, songs []dao.Song) *SearchCore {
26 |
27 | artistCount := len(artists)
28 | artistsDto := make([]*Artist, artistCount)
29 |
30 | for i, a := range artists {
31 | artistsDto[i] = NewArtist(&a)
32 | }
33 |
34 | albumCount := len(albums)
35 | albumsDto := make([]*Album, albumCount)
36 |
37 | for i, a := range albums {
38 | albumsDto[i] = NewAlbum(&a)
39 | }
40 |
41 | songCount := len(songs)
42 | songsDto := make([]*Song, songCount)
43 |
44 | for i, a := range songs {
45 | songsDto[i] = NewSong(&a)
46 | }
47 |
48 | return &SearchCore{
49 | artistsDto,
50 | albumsDto,
51 | songsDto,
52 | }
53 | }
54 |
55 | func NewSearch2Response(artists []dao.Artist, albums []dao.Album, songs []dao.Song) *Search2Response {
56 | core := newSearchResponse(artists, albums, songs)
57 | return &Search2Response{
58 | xml.Name{},
59 | core,
60 | }
61 | }
62 |
63 | func NewSearch3Response(artists []dao.Artist, albums []dao.Album, songs []dao.Song) *Search3Response {
64 | core := newSearchResponse(artists, albums, songs)
65 | return &Search3Response{
66 | xml.Name{},
67 | core,
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/go/subsonic/dto/songDirectory.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type songDirectoryBody struct {
10 | *Directory
11 | *songBody
12 | Name string `xml:"name,attr" json:"name"`
13 | }
14 |
15 | type SongDirectory struct {
16 | XMLName xml.Name `xml:"directory" json:"-"`
17 | *songDirectoryBody
18 | }
19 |
20 | type SongChildDirectory struct {
21 | XMLName xml.Name `xml:"child" json:"-"`
22 | *songDirectoryBody
23 | }
24 |
25 | func newSongDirectoryBody(song *dao.Song) *songDirectoryBody {
26 | return &songDirectoryBody{
27 | Directory: &Directory{
28 | ID: NewSongID(song.ID),
29 | IsDir: false,
30 | Parent: NewAlbumID(song.AlbumID),
31 | },
32 | songBody: newSongBody(song),
33 | Name: song.Title,
34 | }
35 | }
36 |
37 | func NewSongDirectory(song *dao.Song) *SongDirectory {
38 | return &SongDirectory{xml.Name{}, newSongDirectoryBody(song)}
39 | }
40 |
41 | func NewSongChildDirectory(song *dao.Song) *SongChildDirectory {
42 | return &SongChildDirectory{xml.Name{}, newSongDirectoryBody(song)}
43 | }
44 |
--------------------------------------------------------------------------------
/go/subsonic/dto/songsByGenre.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/dao"
7 | )
8 |
9 | type SongsByGenre struct {
10 | XMLName xml.Name `xml:"songsByGenre" json:"-"`
11 | Songs []*Song `xml:"song" json:"song"`
12 | }
13 |
14 | func NewSongsByGenre(songs []dao.Song) *SongsByGenre {
15 |
16 | songDTOs := make([]*Song, len(songs))
17 |
18 | for i, s := range songs {
19 | songDTOs[i] = NewSong(&s)
20 | }
21 | return &SongsByGenre{
22 | Songs: songDTOs,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/go/subsonic/dto/user.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/config"
7 | )
8 |
9 | type User struct {
10 | XMLName xml.Name `xml:"user" json:"-"`
11 | Username string `xml:"username,attr" json:"username"`
12 | Email string `xml:"email,attr" json:"email"`
13 | ScrobblingEnabled bool `xml:"scrobblingEnabled,attr" json:"scrobblingEnabled"`
14 | AdminRole bool `xml:"adminRole,attr" json:"adminRole"`
15 | SettingsRole bool `xml:"settingsRole,attr" json:"settingsRole"`
16 | DownloadRole bool `xml:"downloadRole,attr" json:"downloadRole"`
17 | UploadRole bool `xml:"uploadRole,attr" json:"uploadRole"`
18 | PlaylistRole bool `xml:"playlistRole,attr" json:"playlistRole"`
19 | CoverArtRole bool `xml:"coverArtRole,attr" json:"coverArtRole"`
20 | CommentRole bool `xml:"commentRole,attr" json:"commentRole"`
21 | PodcastRole bool `xml:"podcastRole,attr" json:"podcastRole"`
22 | StreamRole bool `xml:"streamRole,attr" json:"streamRole"`
23 | JukeboxRole bool `xml:"jukeboxRole,attr" json:"jukeboxRole"`
24 | ShareRole bool `xml:"shareRole,attr" json:"shareRole"`
25 | VideoConversionRole bool `xml:"videoConversionRole,attr" json:"videoConversionRole"`
26 | Folder []uint `xml:"folder,attr" json:"folder"`
27 | }
28 |
29 | func NewUser(user config.User) User {
30 | return User{
31 | Username: user.Username,
32 | Email: user.Email,
33 | AdminRole: true,
34 | SettingsRole: true,
35 | DownloadRole: true,
36 | UploadRole: true,
37 | PlaylistRole: true,
38 | StreamRole: true,
39 | Folder: []uint{0},
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/go/subsonic/dto/userCollection.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "encoding/xml"
5 |
6 | "github.com/hednowley/sound/config"
7 | )
8 |
9 | type UserCollection struct {
10 | XMLName xml.Name `xml:"users" json:"-"`
11 | Users []User `xml:"user" json:"user"`
12 | }
13 |
14 | func NewUserCollection(users []config.User) UserCollection {
15 |
16 | col := make([]User, len(users))
17 | for index, user := range users {
18 | col[index] = NewUser(user)
19 | }
20 |
21 | return UserCollection{
22 | Users: col,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/go/subsonic/dto/user_test.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hednowley/sound/config"
7 | )
8 |
9 | func TestUser(t *testing.T) {
10 |
11 | user := config.User{
12 | Username: "dsauid",
13 | Email: "sdfhjfsd@dsid.com",
14 | }
15 |
16 | DTO := NewUser(user)
17 |
18 | xml := `
19 |
20 | `
21 |
22 | json := `
23 | {
24 | "username":"dsauid",
25 | "email":"sdfhjfsd@dsid.com",
26 | "scrobblingEnabled":false,
27 | "adminRole":true,
28 | "settingsRole":true,
29 | "downloadRole":true,
30 | "uploadRole":true,
31 | "playlistRole":true,
32 | "coverArtRole":false,
33 | "commentRole":false,
34 | "podcastRole":false,
35 | "streamRole":true,
36 | "jukeboxRole":false,
37 | "shareRole":false,
38 | "videoConversionRole":false,
39 | "folder":[0]
40 | }
41 | `
42 |
43 | err := CheckSerialisation(DTO, xml, json)
44 | if err != nil {
45 | t.Error(err.Error())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/go/subsonic/handler/deletePlaylist.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 |
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/dao"
9 | "github.com/hednowley/sound/subsonic/api"
10 | "github.com/hednowley/sound/subsonic/dto"
11 | "github.com/hednowley/sound/util"
12 | )
13 |
14 | // NewDeletePlaylistHandler does http://www.subsonic.org/pages/api.jsp#deletePlaylist
15 | func NewDeletePlaylistHandler(dal *dal.DAL) api.Handler {
16 |
17 | return func(params url.Values, context *api.HandlerContext) *api.Response {
18 |
19 | idParam := params.Get("id")
20 | id := util.ParseUint(idParam, 0)
21 | if id == 0 {
22 | return api.NewErrorReponse(dto.MissingParameter, "Required param (id) is missing")
23 | }
24 |
25 | conn, err := dal.Db.GetConn()
26 | if err != nil {
27 | return api.NewErrorReponse(dto.Generic, err.Error())
28 | }
29 | defer conn.Release()
30 |
31 | err = dal.Db.DeletePlaylist(conn, id, context.User.Username)
32 | if err != nil {
33 | if _, ok := err.(*dao.ErrNotFound); ok {
34 | message := fmt.Sprintf("Playlist not found: %v", idParam)
35 | return api.NewErrorReponse(dto.NotFound, message)
36 | }
37 | return api.NewErrorReponse(dto.NotFound, err.Error())
38 | }
39 |
40 | return api.NewSuccessfulReponse(nil)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/go/subsonic/handler/download.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 |
8 | "github.com/hednowley/sound/dal"
9 | "github.com/hednowley/sound/subsonic/api"
10 | "github.com/hednowley/sound/subsonic/dto"
11 | "github.com/hednowley/sound/util"
12 | )
13 |
14 | // NewDownloadHandler does http://www.subsonic.org/pages/api.jsp#download
15 | func NewDownloadHandler(dal *dal.DAL) api.BinaryHandler {
16 |
17 | return func(params url.Values, w *http.ResponseWriter, r *http.Request, _ *api.HandlerContext) *api.Response {
18 |
19 | idParam := params.Get("id")
20 | id := util.ParseUint(idParam, 0)
21 | if id == 0 {
22 | return api.NewErrorReponse(dto.Generic, fmt.Sprintf("Song not found: %v", idParam))
23 | }
24 |
25 | conn, err := dal.Db.GetConn()
26 | if err != nil {
27 | return api.NewErrorReponse(dto.Generic, err.Error())
28 | }
29 | defer conn.Release()
30 |
31 | path := dal.Db.GetSongPath(conn, id)
32 | if path != nil {
33 | return api.NewErrorReponse(dto.Generic, "song not found")
34 | }
35 |
36 | http.ServeFile(*w, r, *path)
37 | return nil
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getAlbum.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/dao"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | "github.com/hednowley/sound/util"
11 | )
12 |
13 | // NewGetAlbumHandler does http://www.subsonic.org/pages/api.jsp#getAlbum
14 | func NewGetAlbumHandler(dal *dal.DAL) api.Handler {
15 |
16 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
17 |
18 | idParam := params.Get("id")
19 | id := util.ParseUint(idParam, 0)
20 | if id == 0 {
21 | return api.NewErrorReponse(dto.MissingParameter, "Required param (id) is missing")
22 | }
23 |
24 | conn, err := dal.Db.GetConn()
25 | if err != nil {
26 | return api.NewErrorReponse(dto.Generic, err.Error())
27 | }
28 | defer conn.Release()
29 |
30 | album, err := dal.Db.GetAlbum(conn, id)
31 | if err != nil {
32 | if _, ok := err.(*dao.ErrNotFound); ok {
33 | return api.NewErrorReponse(dto.NotFound, "Album not found.")
34 | }
35 | return api.NewErrorReponse(dto.Generic, err.Error())
36 | }
37 |
38 | songs, err := dal.Db.GetAlbumSongs(conn, id)
39 | if err != nil {
40 | return api.NewErrorReponse(dto.Generic, err.Error())
41 | }
42 |
43 | return api.NewSuccessfulReponse(dto.NewAlbumWithSongs(album, songs))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getArtist.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/dao"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | "github.com/hednowley/sound/util"
11 | )
12 |
13 | // NewGetArtistHandler does http://www.subsonic.org/pages/api.jsp#getArtist
14 | func NewGetArtistHandler(dal *dal.DAL) api.Handler {
15 |
16 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
17 |
18 | idParam := params.Get("id")
19 | id := util.ParseUint(idParam, 0)
20 | if id == 0 {
21 | return api.NewErrorReponse(dto.MissingParameter, "Required param (id) is missing")
22 | }
23 |
24 | conn, err := dal.Db.GetConn()
25 | if err != nil {
26 | return api.NewErrorReponse(dto.Generic, err.Error())
27 | }
28 | defer conn.Release()
29 |
30 | artist, err := dal.Db.GetArtist(conn, id)
31 | if err != nil {
32 | if _, ok := err.(*dao.ErrNotFound); ok {
33 | return api.NewErrorReponse(dto.NotFound, "Artist not found.")
34 | }
35 | return api.NewErrorReponse(dto.Generic, err.Error())
36 | }
37 |
38 | albums, err := dal.Db.GetAlbumsByArtist(conn, id)
39 | if err != nil {
40 | return api.NewErrorReponse(dto.Generic, err.Error())
41 | }
42 |
43 | return api.NewSuccessfulReponse(dto.NewArtistWithAlbums(artist, albums))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getArtists.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/config"
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | )
11 |
12 | func NewGetArtistsHandler(dal *dal.DAL, conf *config.Config) api.Handler {
13 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
14 | conn, err := dal.Db.GetConn()
15 | if err != nil {
16 | return api.NewErrorReponse(dto.Generic, err.Error())
17 | }
18 | defer conn.Release()
19 |
20 | artists, err := dal.Db.GetArtists(conn)
21 | if err != nil {
22 | return api.NewErrorReponse(0, "Error")
23 | }
24 | return api.NewSuccessfulReponse(dto.NewArtistCollection(artists, conf))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getArtists_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/hednowley/sound/config"
8 | "github.com/hednowley/sound/dal"
9 | "github.com/hednowley/sound/subsonic/api"
10 | "github.com/hednowley/sound/subsonic/dto"
11 | )
12 |
13 | func TestGetArtists(t *testing.T) {
14 |
15 | db := dal.NewMock()
16 | handler := NewGetArtistsHandler(db, &config.Config{})
17 | params := url.Values{}
18 | url.Values.Add(params, "id", "1")
19 |
20 | context := api.HandlerContext{}
21 |
22 | response := handler(params, &context)
23 |
24 | if !response.IsSuccess {
25 | t.Error("Not a success")
26 | }
27 |
28 | _, ok := response.Body.(*dto.ArtistCollection)
29 | if !ok {
30 | t.Error("Not an artist collection")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getCoverArt.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 |
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | "github.com/hednowley/sound/util"
11 | )
12 |
13 | func NewGetCoverArtHandler(dal *dal.DAL) api.BinaryHandler {
14 |
15 | return func(params url.Values, w *http.ResponseWriter, r *http.Request, _ *api.HandlerContext) *api.Response {
16 |
17 | id := params.Get("id")
18 | if id == "" {
19 | return api.NewErrorReponse(dto.NotFound, "Art parameter missing")
20 | }
21 |
22 | sizeParam := params.Get("size")
23 | size := util.ParseUint(sizeParam, 0)
24 |
25 | path := dal.GetArt(id, size)
26 | if path == nil {
27 | return api.NewErrorReponse(dto.Generic, "Art is not available")
28 | }
29 |
30 | http.ServeFile(*w, r, *path)
31 | return nil
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getGenres.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | // NewGetGenresHandler does http://www.subsonic.org/pages/api.jsp#getGenres
12 | func NewGetGenresHandler(dal *dal.DAL) api.Handler {
13 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
14 | conn, err := dal.Db.GetConn()
15 | if err != nil {
16 | return api.NewErrorReponse(dto.Generic, err.Error())
17 | }
18 | defer conn.Release()
19 |
20 | genres, err := dal.Db.GetGenres(conn)
21 | if err != nil {
22 | return api.NewErrorReponse(dto.Generic, err.Error())
23 | }
24 |
25 | return api.NewSuccessfulReponse(dto.NewGenres(genres))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getIndexes.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/config"
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | )
11 |
12 | // NewGetIndexesHandler does http://www.subsonic.org/pages/api.jsp#getIndexes
13 | func NewGetIndexesHandler(dal *dal.DAL, conf *config.Config) api.Handler {
14 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
15 | conn, err := dal.Db.GetConn()
16 | if err != nil {
17 | return api.NewErrorReponse(dto.Generic, err.Error())
18 | }
19 | defer conn.Release()
20 |
21 | artists, err := dal.Db.GetArtists(conn)
22 | if err != nil {
23 | return api.NewErrorReponse(0, "Error")
24 | }
25 | return api.NewSuccessfulReponse(dto.NewIndexCollection(artists, conf))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getLicense.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/subsonic/api"
7 | "github.com/hednowley/sound/subsonic/dto"
8 | )
9 |
10 | func NewGetLicenseHandler() api.Handler {
11 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
12 | return api.NewSuccessfulReponse(dto.NewLicense())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getMusicDirectory_test.go:
--------------------------------------------------------------------------------
1 | package handler_test
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/handler"
10 | )
11 |
12 | func TestGetArtistDirectory(t *testing.T) {
13 |
14 | dal := dal.NewMock()
15 | handler := handler.NewGetMusicDirectoryHandler(dal)
16 | params := url.Values{}
17 | url.Values.Add(params, "id", "artist_1")
18 |
19 | context := api.HandlerContext{}
20 |
21 | handler(params, &context)
22 |
23 | // TODO
24 | }
25 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getMusicFolders.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/provider"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | func NewGetMusicFoldersHandler(providers []provider.Provider) api.Handler {
12 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
13 | return api.NewSuccessfulReponse(dto.NewMusicFolderCollection(providers))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getPlaylist.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 |
7 | "github.com/hednowley/sound/dal"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | "github.com/hednowley/sound/util"
11 | )
12 |
13 | func NewGetPlaylistHandler(dal *dal.DAL) api.Handler {
14 |
15 | return func(params url.Values, context *api.HandlerContext) *api.Response {
16 |
17 | idParam := params.Get("id")
18 | id := util.ParseUint(idParam, 0)
19 | if id == 0 {
20 | message := fmt.Sprintf("Playlist not found: %v", idParam)
21 | return api.NewErrorReponse(dto.Generic, message)
22 | }
23 |
24 | conn, err := dal.Db.GetConn()
25 | if err != nil {
26 | return api.NewErrorReponse(dto.Generic, err.Error())
27 | }
28 | defer conn.Release()
29 |
30 | p, err := dal.Db.GetPlaylist(conn, id, context.User.Username)
31 | if err != nil {
32 | return api.NewErrorReponse(dto.Generic, err.Error())
33 | }
34 |
35 | // Pretend that this playlist is owned by the requestor.
36 | // Allows anyone to edit a public playlist (against the intention of
37 | // the Subsonic API).
38 | p.Owner = context.User.Username
39 |
40 | songs, err := dal.Db.GetPlaylistSongs(conn, id, context.User.Username)
41 | if err != nil {
42 | return api.NewErrorReponse(dto.Generic, err.Error())
43 | }
44 |
45 | return api.NewSuccessfulReponse(dto.NewPlaylist(p, songs))
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getPlaylists.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | // NewGetPlaylistsHandler does http://www.subsonic.org/pages/api.jsp#getPlaylists
12 | func NewGetPlaylistsHandler(dal *dal.DAL) api.Handler {
13 |
14 | return func(params url.Values, context *api.HandlerContext) *api.Response {
15 | conn, err := dal.Db.GetConn()
16 | if err != nil {
17 | return api.NewErrorReponse(dto.Generic, err.Error())
18 | }
19 | defer conn.Release()
20 |
21 | playlists, err := dal.Db.GetPlaylists(conn, context.User.Username)
22 | if err != nil {
23 | return api.NewErrorReponse(dto.Generic, err.Error())
24 | }
25 |
26 | // Pretend that all accessible playlists are owned by the requestor.
27 | // Allows anyone to edit a public playlist (against the intention of
28 | // the Subsonic API).
29 | for i := range playlists {
30 | playlists[i].Owner = context.User.Username
31 | }
32 |
33 | return api.NewSuccessfulReponse(dto.NewPlaylistCollection(playlists))
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getRandomSongs.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | "github.com/hednowley/sound/util"
10 | )
11 |
12 | // NewGetRandomSongsHandler does http://www.subsonic.org/pages/api.jsp#getRandomSongs
13 | func NewGetRandomSongsHandler(dal *dal.DAL) api.Handler {
14 |
15 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
16 |
17 | sizeParam := params.Get("size")
18 | size := util.ParseUint(sizeParam, 10)
19 |
20 | genre := params.Get("genre")
21 |
22 | fromParam := params.Get("fromYear")
23 | from := util.ParseUint(fromParam, 0)
24 |
25 | toParam := params.Get("toYear")
26 | to := util.ParseUint(toParam, 0)
27 |
28 | conn, err := dal.Db.GetConn()
29 | if err != nil {
30 | return api.NewErrorReponse(dto.Generic, err.Error())
31 | }
32 | defer conn.Release()
33 |
34 | songs, err := dal.Db.GetRandomSongs(conn, size, from, to, genre)
35 | if err != nil {
36 | return api.NewErrorReponse(dto.Generic, err.Error())
37 | }
38 |
39 | return api.NewSuccessfulReponse(dto.NewRandomSongs(songs))
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getSong.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/dao"
8 | "github.com/hednowley/sound/subsonic/api"
9 | "github.com/hednowley/sound/subsonic/dto"
10 | "github.com/hednowley/sound/util"
11 | )
12 |
13 | // NewGetSongHandler does http://www.subsonic.org/pages/api.jsp#getSong
14 | func NewGetSongHandler(dal *dal.DAL) api.Handler {
15 |
16 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
17 |
18 | idParam := params.Get("id")
19 | id := util.ParseUint(idParam, 0)
20 | if id == 0 {
21 | return api.NewErrorReponse(dto.MissingParameter, "Required param (id) is missing")
22 | }
23 |
24 | conn, err := dal.Db.GetConn()
25 | if err != nil {
26 | return api.NewErrorReponse(dto.Generic, err.Error())
27 | }
28 | defer conn.Release()
29 |
30 | file, err := dal.Db.GetSong(conn, id)
31 | if err != nil {
32 | if dao.IsErrNotFound(err) {
33 | return api.NewErrorReponse(dto.NotFound, "Song not found.")
34 | }
35 | return api.NewErrorReponse(dto.Generic, err.Error())
36 | }
37 |
38 | return api.NewSuccessfulReponse(dto.NewSong(file))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getSongsByGenre.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | "github.com/hednowley/sound/util"
10 | )
11 |
12 | func NewGetSongsByGenreHandler(dal *dal.DAL) api.Handler {
13 |
14 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
15 |
16 | genre := params.Get("genre")
17 | if len(genre) == 0 {
18 | return api.NewErrorReponse(dto.MissingParameter, "Required param (genre) is missing")
19 | }
20 |
21 | countParam := params.Get("count")
22 | count := util.ParseUint(countParam, 10)
23 |
24 | offsetParam := params.Get("offset")
25 | offset := util.ParseUint(offsetParam, 0)
26 |
27 | conn, err := dal.Db.GetConn()
28 | if err != nil {
29 | return api.NewErrorReponse(dto.Generic, err.Error())
30 | }
31 | defer conn.Release()
32 |
33 | songs, err := dal.Db.GetSongsByGenre(conn, genre, offset, count)
34 | if err != nil {
35 | return api.NewErrorReponse(dto.Generic, err.Error())
36 | }
37 |
38 | return api.NewSuccessfulReponse(dto.NewSongsByGenre(songs))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getUser.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/config"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | func NewGetUserHandler(config *config.Config) api.Handler {
12 |
13 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
14 |
15 | username := params.Get("username")
16 | if len(username) == 0 {
17 | return api.NewErrorReponse(dto.NotFound, "No username.")
18 | }
19 |
20 | for _, user := range config.Users {
21 | if user.Username == username {
22 | return api.NewSuccessfulReponse(dto.NewUser(user))
23 | }
24 | }
25 |
26 | return api.NewErrorReponse(dto.NotFound, "User not found.")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/go/subsonic/handler/getUsers.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/config"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | func NewGetUsersHandler(config *config.Config) api.Handler {
12 |
13 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
14 | return api.NewSuccessfulReponse(dto.NewUserCollection(config.Users))
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/go/subsonic/handler/ping.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/subsonic/api"
7 | )
8 |
9 | // NewPingHandler is a handler for responding to ping requests.
10 | // It replies to any request with an empty success response.
11 | func NewPingHandler() api.Handler {
12 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
13 | return api.NewEmptyReponse()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/go/subsonic/handler/ping_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/hednowley/sound/subsonic/api"
8 | )
9 |
10 | func TestPing(t *testing.T) {
11 |
12 | handler := NewPingHandler()
13 | response := handler(url.Values{}, &api.HandlerContext{})
14 |
15 | if !response.IsSuccess {
16 | t.Error()
17 | }
18 |
19 | if response.Body != nil {
20 | t.Error()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/go/subsonic/handler/scan.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/provider"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | )
10 |
11 | func NewStartScanHandler(dal *provider.Scanner) api.Handler {
12 |
13 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
14 | go dal.StartAllScans(false, false)
15 | r := dto.NewScanStatus(dal.GetScanStatus(), dal.GetScanFileCount())
16 | return api.NewSuccessfulReponse(r)
17 | }
18 | }
19 |
20 | func NewGetScanStatusHandler(dal *provider.Scanner) api.Handler {
21 |
22 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
23 | r := dto.NewScanStatus(dal.GetScanStatus(), dal.GetScanFileCount())
24 | return api.NewSuccessfulReponse(r)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/go/subsonic/handler/star.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/hednowley/sound/dal"
7 | "github.com/hednowley/sound/subsonic/api"
8 | "github.com/hednowley/sound/subsonic/dto"
9 | "github.com/hednowley/sound/util"
10 | )
11 |
12 | func NewStarHandler(dal *dal.DAL, star bool) api.Handler {
13 |
14 | return func(params url.Values, _ *api.HandlerContext) *api.Response {
15 |
16 | param := params.Get("id")
17 | id := util.ParseUint(param, 0)
18 | if id == 0 {
19 | err := dal.StarSong(id, star)
20 | if err != nil {
21 | return api.NewErrorReponse(dto.Generic, err.Error())
22 | }
23 |
24 | return api.NewEmptyReponse()
25 | }
26 |
27 | param = params.Get("albumId")
28 | id = util.ParseUint(param, 0)
29 | if id == 0 {
30 | err := dal.StarAlbum(id, star)
31 | if err != nil {
32 | return api.NewErrorReponse(dto.Generic, err.Error())
33 | }
34 |
35 | return api.NewEmptyReponse()
36 | }
37 |
38 | param = params.Get("artistId")
39 | id = util.ParseUint(param, 0)
40 | if id == 0 {
41 | err := dal.StarArtist(id, star)
42 | if err != nil {
43 | return api.NewErrorReponse(dto.Generic, err.Error())
44 | }
45 |
46 | return api.NewEmptyReponse()
47 | }
48 |
49 | return api.NewErrorReponse(dto.Generic, "Missing parameter")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/go/subsonic/handler/updatePlaylist_test.go:
--------------------------------------------------------------------------------
1 | package handler_test
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/hednowley/sound/config"
8 | "github.com/hednowley/sound/dal"
9 | "github.com/hednowley/sound/subsonic/api"
10 | "github.com/hednowley/sound/subsonic/handler"
11 | )
12 |
13 | func TestUpdatePlaylist(t *testing.T) {
14 |
15 | db := dal.NewMock()
16 |
17 | handler := handler.NewUpdatePlaylistHandler(db)
18 | params := url.Values{}
19 | params.Add("playlistId", "1")
20 | params.Add("name", "new_name")
21 |
22 | context := api.HandlerContext{
23 | User: &config.User{
24 | Username: "tommy",
25 | },
26 | }
27 |
28 | response := handler(params, &context)
29 |
30 | if !response.IsSuccess {
31 | t.Error("Should succeed")
32 | }
33 |
34 | if response.Body != nil {
35 | t.Error("Should have no body")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/go/testdata/beetslib.blb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/testdata/beetslib.blb
--------------------------------------------------------------------------------
/go/testdata/config.yaml:
--------------------------------------------------------------------------------
1 | secret: "changeme"
2 |
3 | port: 3684
4 |
5 | art path: "~/temp/art"
6 |
7 | art sizes:
8 | - 100
9 | - 200
10 |
11 | db: "host=localhost port=5432 user=postgres password=sound dbname=sound sslmode=disable"
12 |
13 | log config: "log-config.xml"
14 |
15 | ignored articles:
16 | - A
17 | - The
18 | - El
19 | - Los
20 |
21 | filesystem:
22 | - path: "~/temp/my music"
23 | name: "Gertrude's bangers"
24 | extensions:
25 | - mp3
26 | - flac
27 |
28 | - path: "~/temp2/my music"
29 | name: "Choons"
30 | extensions:
31 | - docx
32 |
33 | beets:
34 | - database: "~/beets/beetslib.blb"
35 | name: "My beets music"
36 |
37 | users:
38 | - username: gertrude
39 | password: changeme
40 |
41 | access control allow origin: "*"
42 |
43 | websocket ticket expiry: 30
44 |
--------------------------------------------------------------------------------
/go/testdata/dao/albums.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | artist_id: 1
3 | name: "album_1"
4 | created: 2018-06-12T11:11:11Z
5 | disambiguator: ""
6 | starred: false
7 |
8 | - id: 2
9 | artist_id: 6
10 | name: "album_without_art"
11 | created: 2018-06-12T11:11:11Z
12 | disambiguator: ""
13 | starred: false
14 |
15 | - id: 3
16 | artist_id: 1
17 | name: "album_without_genre"
18 | created: 2018-06-12T11:11:11Z
19 | disambiguator: ""
20 | starred: false
21 |
22 | - id: 4
23 | artist_id: 1
24 | name: "album_without_year"
25 | created: 2018-06-12T11:11:11Z
26 | disambiguator: ""
27 | starred: false
28 |
--------------------------------------------------------------------------------
/go/testdata/dao/artists.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: "artist_1"
3 | starred: false
4 |
5 | - id: 2
6 | name: "artist_2"
7 | starred: false
8 |
9 | - id: 4
10 | name: "artist_4"
11 | starred: false
12 |
13 | - id: 5
14 | name: "beethoven"
15 | starred: false
16 |
17 | - id: 6
18 | name: "artist_without_art"
19 | starred: false
20 |
21 | - id: 7
22 | name: "artist_without_albums"
23 | starred: false
24 |
--------------------------------------------------------------------------------
/go/testdata/dao/arts.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | hash: "hash_1"
3 | path: "art_1.png"
4 |
--------------------------------------------------------------------------------
/go/testdata/dao/genres.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: "genre_1"
3 |
4 | - id: 2
5 | name: "genre_2"
6 |
7 | - id: 4
8 | name: "genre_4"
9 |
--------------------------------------------------------------------------------
/go/testdata/dao/playlist_entries.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | playlist_id: 1
3 | song_id: 1
4 | index: 0
5 |
6 | - id: 3
7 | playlist_id: 1
8 | song_id: 1
9 | index: 5
10 |
11 | - id: 4
12 | playlist_id: 1
13 | song_id: 14
14 | index: 2
15 |
16 | - id: 10
17 | playlist_id: 1
18 | song_id: 2
19 | index: 6
20 |
--------------------------------------------------------------------------------
/go/testdata/dao/playlists.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: "playlist_1"
3 | comment: "comment_1"
4 | public: true
5 | comment: "comment_1"
6 | created: 2018-06-12T11:11:11Z
7 | changed: 2018-06-12T11:11:11Z
8 | owner: "tommy"
9 |
--------------------------------------------------------------------------------
/go/testdata/music/1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/testdata/music/1.mp3
--------------------------------------------------------------------------------
/go/testdata/music/2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/testdata/music/2.mp3
--------------------------------------------------------------------------------
/go/testdata/music/lost.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/testdata/music/lost.txt
--------------------------------------------------------------------------------
/go/testdata/music/subfolder/3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hednowley/sound/3fa1ee0a0b1e3d15d12557429fa674f9e19ee93d/go/testdata/music/subfolder/3.mp3
--------------------------------------------------------------------------------
/go/todo.txt:
--------------------------------------------------------------------------------
1 | - ignored articles
2 | - Get duration and bitrate from files
3 | - transcoding
4 | - scheduling
5 | - https://golang.org/pkg/sync/#Pool
6 | - Deploy to web server
7 | - Make scans stoppable
8 | - Test beets provider
9 | - Disambiguation (find first different fields?)
10 | - Play counts
11 | - Implement getartistinfo2
12 | - SQL injection in search
13 | - Speed up artist first-letter code
14 | - Artists with different capitalisations
15 | - Think about emopty album names, genre etc
16 | - Podcasts
17 | - Add real providers to user folders
--------------------------------------------------------------------------------
/go/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | // Min returns the smaller of two uints.
9 | func Min(a int, b int) int {
10 | if a > b {
11 | return b
12 | }
13 | return a
14 | }
15 |
16 | // Max returns the larger of two uints.
17 | func Max(a int, b int) int {
18 | if a < b {
19 | return b
20 | }
21 | return a
22 | }
23 |
24 | // Contains checks whether a slice of uints contains the given value.
25 | func ContainsUint(slice []uint, value uint) bool {
26 | for _, v := range slice {
27 | if v == value {
28 | return true
29 | }
30 | }
31 | return false
32 | }
33 |
34 | func ContainsString(slice []string, value string) bool {
35 | for _, v := range slice {
36 | if v == value {
37 | return true
38 | }
39 | }
40 | return false
41 | }
42 |
43 | func FormatUint(i uint) string {
44 | return strconv.FormatUint(uint64(i), 10)
45 | }
46 |
47 | // ParseUint tries to parse a string into a uint. If this is not possible
48 | // then the given default is returned instead.
49 | func ParseUint(param string, defaultValue uint) uint {
50 |
51 | id, err := strconv.ParseUint(param, 10, 32)
52 | if err != nil {
53 | return defaultValue
54 | }
55 | return uint(id)
56 | }
57 |
58 | // ParseBool tries to parse a string into a bool. If this is not possible
59 | // then a nil pointer is returned instead.
60 | func ParseBool(param string) *bool {
61 |
62 | param = strings.ToLower(param)
63 | if param == "true" || param == "1" {
64 | b := true
65 | return &b
66 | }
67 | if param == "false" || param == "0" {
68 | b := false
69 | return &b
70 | }
71 |
72 | return nil
73 | }
74 |
--------------------------------------------------------------------------------
/go/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hednowley/sound/util"
7 | )
8 |
9 | func TestMin(t *testing.T) {
10 |
11 | m := util.Min(100, 80)
12 | if m != 80 {
13 | t.Error()
14 | }
15 |
16 | m = util.Min(-10, -20)
17 | if m != -20 {
18 | t.Error()
19 | }
20 |
21 | m = util.Min(-5, -5)
22 | if m != -5 {
23 | t.Error()
24 | }
25 | }
26 |
27 | func TestMax(t *testing.T) {
28 |
29 | m := util.Max(100, 80)
30 | if m != 100 {
31 | t.Error()
32 | }
33 |
34 | m = util.Max(-10, -20)
35 | if m != -10 {
36 | t.Error()
37 | }
38 |
39 | m = util.Max(-5, -5)
40 | if m != -5 {
41 | t.Error()
42 | }
43 | }
44 |
45 | func TestContains(t *testing.T) {
46 |
47 | s := []uint{}
48 | if util.ContainsUint(s, 0) {
49 | t.Error()
50 | }
51 |
52 | s = []uint{5, 80}
53 | if util.ContainsUint(s, 10) {
54 | t.Error()
55 | }
56 | if !util.ContainsUint(s, 80) {
57 | t.Error()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/sound.code-workspace:
--------------------------------------------------------------------------------
1 | t{
2 | "folders": [
3 | {
4 | "path": "."
5 | },
6 | {
7 | "path": "go"
8 | },
9 | {
10 | "path": "elm"
11 | }
12 | ],
13 | "settings": {}
14 | }
15 |
--------------------------------------------------------------------------------