diff --git a/engine/media_streamer.go b/engine/media_streamer.go index e6dc265ae..70f7ac846 100644 --- a/engine/media_streamer.go +++ b/engine/media_streamer.go @@ -150,7 +150,7 @@ func (m *transcodedMediaStream) Read(p []byte) (n int, err error) { // a Seek happens. This is ok-ish for audio, but would kill the server for video. func (m *transcodedMediaStream) Seek(offset int64, whence int) (int64, error) { size := int64((m.mf.Duration)*m.bitRate*1000) / 8 - log.Trace(m.ctx, "Seeking file", "path", m.mf.Path, "offset", offset, "whence", whence, "size", size) + log.Trace(m.ctx, "Seeking transcoded stream", "path", m.mf.Path, "offset", offset, "whence", whence, "size", size) switch whence { case io.SeekEnd: diff --git a/ui/package-lock.json b/ui/package-lock.json index 291e80ba0..ece79a2bd 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -2491,6 +2491,14 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" }, + "add-dom-event-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", + "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "requires": { + "object-assign": "4.x" + } + }, "address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -4039,11 +4047,24 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "component-classes": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", + "integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=", + "requires": { + "component-indexof": "0.0.3" + } + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, + "component-indexof": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz", + "integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=" + }, "compose-function": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", @@ -4352,6 +4373,15 @@ "urix": "^0.1.0" } }, + "css-animation": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.6.1.tgz", + "integrity": "sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==", + "requires": { + "babel-runtime": "6.x", + "component-classes": "^1.2.5" + } + }, "css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -4906,6 +4936,11 @@ "esutils": "^2.0.2" } }, + "dom-align": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.10.4.tgz", + "integrity": "sha512-wytDzaru67AmqFOY4B9GUb/hrwWagezoYYK97D/vpK+ezg+cnuZO0Q2gltUPa7KfNmIqfRIYVCF8UhRDEHAmgQ==" + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -4992,6 +5027,11 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=" + }, "downshift": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.2.7.tgz", @@ -7564,6 +7604,11 @@ "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" }, + "is-mobile": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-2.1.0.tgz", + "integrity": "sha512-M5OhlZwh+aTlmRUvDg0Wq3uWVNa+w4DyZ2SjbrS+BhSLu9Po+JXHendC305ZEu+Hh7lywb19Zu4kYXu3L1Oo8A==" + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -13046,6 +13091,92 @@ } } }, + "rc-align": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.5.tgz", + "integrity": "sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==", + "requires": { + "babel-runtime": "^6.26.0", + "dom-align": "^1.7.0", + "prop-types": "^15.5.8", + "rc-util": "^4.0.4" + } + }, + "rc-animate": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.2.tgz", + "integrity": "sha512-cE/A7piAzoWFSgUD69NmmMraqCeqVBa51UErod8NS3LUEqWfppSVagHfa0qHAlwPVPiIBg3emRONyny3eiH0Dg==", + "requires": { + "babel-runtime": "6.x", + "classnames": "^2.2.6", + "css-animation": "^1.3.2", + "prop-types": "15.x", + "raf": "^3.4.0", + "rc-util": "^4.15.3", + "react-lifecycles-compat": "^3.0.4" + } + }, + "rc-slider": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-8.7.1.tgz", + "integrity": "sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g==", + "requires": { + "babel-runtime": "6.x", + "classnames": "^2.2.5", + "prop-types": "^15.5.4", + "rc-tooltip": "^3.7.0", + "rc-util": "^4.0.4", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0", + "warning": "^4.0.3" + } + }, + "rc-switch": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-1.9.0.tgz", + "integrity": "sha512-Isas+egaK6qSk64jaEw4GgPStY4umYDbT7ZY93bZF1Af+b/JEsKsJdNOU2qG3WI0Z6tXo2DDq0kJCv8Yhu0zww==", + "requires": { + "classnames": "^2.2.1", + "prop-types": "^15.5.6", + "react-lifecycles-compat": "^3.0.4" + } + }, + "rc-tooltip": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-3.7.3.tgz", + "integrity": "sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww==", + "requires": { + "babel-runtime": "6.x", + "prop-types": "^15.5.8", + "rc-trigger": "^2.2.2" + } + }, + "rc-trigger": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-2.6.5.tgz", + "integrity": "sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==", + "requires": { + "babel-runtime": "6.x", + "classnames": "^2.2.6", + "prop-types": "15.x", + "rc-align": "^2.4.0", + "rc-animate": "2.x", + "rc-util": "^4.4.0", + "react-lifecycles-compat": "^3.0.4" + } + }, + "rc-util": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.19.0.tgz", + "integrity": "sha512-mptALlLwpeczS3nrv83DbwJNeupolbuvlIEjcvimSiWI8NUBjpF0HgG3kWp1RymiuiRCNm9yhaXqDz0a99dpgQ==", + "requires": { + "add-dom-event-listener": "^1.1.0", + "babel-runtime": "6.x", + "prop-types": "^15.5.10", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0" + } + }, "react": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", @@ -13231,6 +13362,23 @@ "scheduler": "^0.18.0" } }, + "react-drag-listview": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/react-drag-listview/-/react-drag-listview-0.1.6.tgz", + "integrity": "sha512-0nSWkR1bMLKgLZIYY2YVURYapppzy46FNSs9uAcCxceo2lnajngzLQ3tBgWaTjKTlWMXD0MAcDUWFDYdqMPYUg==", + "requires": { + "prop-types": "^15.5.8" + } + }, + "react-draggable": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-3.3.2.tgz", + "integrity": "sha512-oaz8a6enjbPtx5qb0oDWxtDNuybOylvto1QLydsXgKmwT7e3GXC2eMVDwEMIUYJIFqVG72XpOv673UuuAq6LhA==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-dropzone": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.1.tgz", @@ -13263,11 +13411,40 @@ "@babel/runtime": "^7.4.5" } }, + "react-icon-base": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-icon-base/-/react-icon-base-2.1.0.tgz", + "integrity": "sha1-oZbjP98eeqof2jrvu2i9rZ6Cp50=" + }, + "react-icons": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-2.2.7.tgz", + "integrity": "sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==", + "requires": { + "react-icon-base": "2.1.0" + } + }, "react-is": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-jinke-music-player": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/react-jinke-music-player/-/react-jinke-music-player-4.7.2.tgz", + "integrity": "sha512-r2P1gf7nsOBBXqVaKbN73POomWXAYiHuOq5q6AIiUPCVvKx19pCiOsVqwN0vB3kN5tK3Vypm1tO0GkFBVVK11Q==", + "requires": { + "classnames": "^2.2.6", + "downloadjs": "^1.4.7", + "is-mobile": "^2.1.0", + "prop-types": "^15.7.2", + "rc-slider": "^8.7.1", + "rc-switch": "^1.9.0", + "react-drag-listview": "^0.1.6", + "react-draggable": "^3.3.2", + "react-icons": "^2.2.5" + } + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -14262,6 +14439,11 @@ } } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/ui/package.json b/ui/package.json index c6275ae00..98de4890a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "react": "^16.12.0", "react-admin": "^3.1.2", "react-dom": "^16.12.0", + "react-jinke-music-player": "^4.7.2", "react-scripts": "3.3.0" }, "scripts": { diff --git a/ui/src/App.js b/ui/src/App.js index 2448597a3..65dad963c 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,31 +1,41 @@ -// in src/App.js import React from 'react' import { Admin, Resource } from 'react-admin' import dataProvider from './dataProvider' import authProvider from './authProvider' -import { Login, Layout, DarkTheme } from './layout' +import { DarkTheme, Layout, Login } from './layout' import user from './user' import song from './song' import album from './album' import artist from './artist' import { createMuiTheme } from '@material-ui/core/styles' +import { Player, playQueueReducer } from './player' const theme = createMuiTheme(DarkTheme) const App = () => ( - - {(permissions) => [ - , - , - , - permissions === 'admin' ? : null - ]} - + <> +
+ + {(permissions) => [ + , + , + , + permissions === 'admin' ? : null, + + ]} + +
+ ) export default App diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js index 92ad99e7a..1fd511703 100644 --- a/ui/src/album/AlbumList.js +++ b/ui/src/album/AlbumList.js @@ -50,7 +50,6 @@ const AlbumList = (props) => ( exporter={false} bulkActionButtons={false} filters={} - perPage={15} > } rowClick={albumRowClick}> diff --git a/ui/src/artist/ArtistList.js b/ui/src/artist/ArtistList.js index ba5e7e3aa..860ee0adc 100644 --- a/ui/src/artist/ArtistList.js +++ b/ui/src/artist/ArtistList.js @@ -28,7 +28,6 @@ const ArtistList = (props) => ( exporter={false} bulkActionButtons={false} filters={} - perPage={15} > diff --git a/ui/src/layout/Menu.js b/ui/src/layout/Menu.js index 9e8229a51..85e459544 100644 --- a/ui/src/layout/Menu.js +++ b/ui/src/layout/Menu.js @@ -1,4 +1,3 @@ -// in src/Menu.js import React, { useState, createElement } from 'react' import { useSelector } from 'react-redux' import { useMediaQuery } from '@material-ui/core' diff --git a/ui/src/player/Player.js b/ui/src/player/Player.js new file mode 100644 index 000000000..e8553f44c --- /dev/null +++ b/ui/src/player/Player.js @@ -0,0 +1,66 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useAuthState } from 'react-admin' +import ReactJkMusicPlayer from 'react-jinke-music-player' +import 'react-jinke-music-player/assets/index.css' +import { syncQueue } from './queue' + +const defaultOptions = { + bounds: 'body', + mode: 'full', + autoPlay: true, + preload: true, + autoPlayInitLoadPlayList: true, + clearPriorAudioLists: false, + showDownload: false, + showReload: false, + glassBg: false, + showThemeSwitch: false, + playModeText: { + order: 'order', + orderLoop: 'orderLoop', + singleLoop: 'singleLoop', + shufflePlay: 'shufflePlay' + }, + defaultPosition: { + top: 300, + left: 120 + } +} + +const addQueueToOptions = (queue) => { + return { + ...defaultOptions, + autoPlay: true, + clearPriorAudioLists: queue.clear, + audioLists: queue.queue.map((item) => item) + } +} + +const Player = () => { + const dispatch = useDispatch() + const queue = useSelector((state) => state.queue) + const options = addQueueToOptions(queue) + const { authenticated } = useAuthState() + + const OnAudioListsChange = (currentPlayIndex, audioLists) => { + dispatch(syncQueue(audioLists)) + } + + const OnAudioProgress = (info) => { + const progress = (info.currentTime / info.duration) * 100 + } + + if (authenticated && options.audioLists.length > 0) { + return ( + + ) + } + return
+} + +export default Player diff --git a/ui/src/player/index.js b/ui/src/player/index.js new file mode 100644 index 000000000..38bef76d5 --- /dev/null +++ b/ui/src/player/index.js @@ -0,0 +1,4 @@ +import Player from './Player' +import { addTrack, setTrack, playQueueReducer } from './queue' + +export { Player, addTrack, setTrack, playQueueReducer } diff --git a/ui/src/player/queue.js b/ui/src/player/queue.js new file mode 100644 index 000000000..9f5ffcaee --- /dev/null +++ b/ui/src/player/queue.js @@ -0,0 +1,50 @@ +import 'react-jinke-music-player/assets/index.css' + +const PLAYER_ADD_TRACK = 'PLAYER_ADD_TRACK' +const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK' +const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE' + +const mapToAudioLists = (item) => ({ + id: item.id, + name: item.title, + singer: item.artist, + cover: `/rest/getCoverArt.view?u=admin&p=enc:73756e6461&f=json&v=1.8.0&c=Jamstash&size=300&id=${item.id}`, + musicSrc: `/rest/stream.view?u=admin&p=enc:73756e6461&f=json&v=1.8.0&c=Jamstash&id=${ + item.id + }&ts=${new Date().getTime()}` +}) + +const addTrack = (data) => ({ + type: PLAYER_ADD_TRACK, + data +}) + +const setTrack = (data) => ({ + type: PLAYER_SET_TRACK, + data +}) + +const syncQueue = (data) => ({ + type: PLAYER_SYNC_QUEUE, + data +}) + +const playQueueReducer = ( + previousState = { queue: [], clear: true }, + { type, data } +) => { + switch (type) { + case PLAYER_ADD_TRACK: + const queue = previousState.queue + queue.push(mapToAudioLists(data)) + return { queue, clear: false } + case PLAYER_SET_TRACK: + return { queue: [mapToAudioLists(data)], clear: true } + case PLAYER_SYNC_QUEUE: + return { queue: data, clear: false } + default: + return previousState + } +} + +export { addTrack, setTrack, syncQueue, playQueueReducer } diff --git a/ui/src/song/AddToQueueButton.js b/ui/src/song/AddToQueueButton.js new file mode 100644 index 000000000..ad952c627 --- /dev/null +++ b/ui/src/song/AddToQueueButton.js @@ -0,0 +1,23 @@ +import React from 'react' +import { Button, useDataProvider, useUnselectAll } from 'react-admin' +import { useDispatch } from 'react-redux' +import { addTrack } from '../player' + +const AddToQueueButton = ({ selectedIds }) => { + const dispatch = useDispatch() + const dataProvider = useDataProvider() + const unselectAll = useUnselectAll() + const addToQueue = () => { + selectedIds.forEach((id) => { + dataProvider.getOne('song', { id }).then((response) => { + console.log(response.data) + dispatch(addTrack(response.data)) + }) + }) + unselectAll('song') + } + + return