feat: initial integration of react-jinke-music-player

This commit is contained in:
Deluan 2020-02-04 09:26:54 -05:00
parent 220ffd5324
commit 4a82a6cb02
13 changed files with 417 additions and 44 deletions

View File

@ -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:

182
ui/package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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 = () => (
<Admin
theme={theme}
dataProvider={dataProvider}
authProvider={authProvider}
layout={Layout}
loginPage={Login}
>
{(permissions) => [
<Resource name="artist" {...artist} options={{ subMenu: 'library' }} />,
<Resource name="album" {...album} options={{ subMenu: 'library' }} />,
<Resource name="song" {...song} options={{ subMenu: 'library' }} />,
permissions === 'admin' ? <Resource name="user" {...user} /> : null
]}
</Admin>
<>
<div>
<Admin
theme={theme}
customReducers={{ queue: playQueueReducer }}
dataProvider={dataProvider}
authProvider={authProvider}
layout={Layout}
loginPage={Login}
>
{(permissions) => [
<Resource
name="artist"
{...artist}
options={{ subMenu: 'library' }}
/>,
<Resource name="album" {...album} options={{ subMenu: 'library' }} />,
<Resource name="song" {...song} options={{ subMenu: 'library' }} />,
permissions === 'admin' ? <Resource name="user" {...user} /> : null,
<Player />
]}
</Admin>
</div>
</>
)
export default App

View File

@ -50,7 +50,6 @@ const AlbumList = (props) => (
exporter={false}
bulkActionButtons={false}
filters={<AlbumFilter />}
perPage={15}
>
<Datagrid expand={<AlbumDetails />} rowClick={albumRowClick}>
<TextField source="name" />

View File

@ -28,7 +28,6 @@ const ArtistList = (props) => (
exporter={false}
bulkActionButtons={false}
filters={<ArtistFilter />}
perPage={15}
>
<Datagrid rowClick={artistRowClick}>
<TextField source="name" />

View File

@ -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'

66
ui/src/player/Player.js Normal file
View File

@ -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 (
<ReactJkMusicPlayer
{...options}
onAudioListsChange={OnAudioListsChange}
onAudioProgress={OnAudioProgress}
/>
)
}
return <div />
}
export default Player

4
ui/src/player/index.js Normal file
View File

@ -0,0 +1,4 @@
import Player from './Player'
import { addTrack, setTrack, playQueueReducer } from './queue'
export { Player, addTrack, setTrack, playQueueReducer }

50
ui/src/player/queue.js Normal file
View File

@ -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 }

View File

@ -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 <Button color="secondary" label="Add To Queue" onClick={addToQueue} />
}
export default AddToQueueButton

30
ui/src/song/PlayButton.js Normal file
View File

@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import PlayArrowIcon from '@material-ui/icons/PlayArrow'
import { IconButton } from '@material-ui/core'
import { useDispatch } from 'react-redux'
import { setTrack } from '../player'
const defaultIcon = <PlayArrowIcon fontSize="small" />
const PlayButton = ({
record,
icon = defaultIcon,
action = setTrack,
...rest
}) => {
const dispatch = useDispatch()
return (
<IconButton onClick={() => dispatch(action(record))} {...rest}>
{icon}
</IconButton>
)
}
PlayButton.propTypes = {
record: PropTypes.any,
icon: PropTypes.element,
action: PropTypes.func
}
export default PlayButton

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { Fragment } from 'react'
import {
BooleanField,
Datagrid,
@ -7,12 +7,14 @@ import {
List,
NumberField,
SearchInput,
TextInput,
Show,
SimpleShowLayout,
TextField
TextField,
TextInput
} from 'react-admin'
import { BitrateField, DurationField, Title } from '../common'
import AddToQueueButton from './AddToQueueButton'
import PlayButton from './PlayButton'
const SongFilter = (props) => (
<Filter {...props}>
@ -22,6 +24,12 @@ const SongFilter = (props) => (
</Filter>
)
const SongBulkActionButtons = (props) => (
<Fragment>
<AddToQueueButton {...props} />
</Fragment>
)
const SongDetails = (props) => {
return (
<Show {...props} title=" ">
@ -37,26 +45,28 @@ const SongDetails = (props) => {
)
}
const SongList = (props) => (
<List
{...props}
title={<Title subTitle={'Songs'} />}
sort={{ field: 'title', order: 'ASC' }}
exporter={false}
bulkActionButtons={false}
filters={<SongFilter />}
perPage={15}
>
<Datagrid expand={<SongDetails />}>
<TextField source="title" />
<TextField source="album" />
<TextField source="artist" />
<NumberField label="Track #" source="trackNumber" />
<NumberField label="Disc #" source="discNumber" />
<TextField source="year" />
<DurationField label="Time" source="duration" />
</Datagrid>
</List>
)
const SongList = (props) => {
return (
<List
{...props}
title={<Title subTitle={'Songs'} />}
sort={{ field: 'title', order: 'ASC' }}
exporter={false}
bulkActionButtons={<SongBulkActionButtons />}
filters={<SongFilter />}
>
<Datagrid expand={<SongDetails />}>
<PlayButton {...props} />
<TextField source="title" />
<TextField source="album" />
<TextField source="artist" />
<NumberField label="Track #" source="trackNumber" />
<NumberField label="Disc #" source="discNumber" />
<TextField source="year" />
<DurationField label="Time" source="duration" />
</Datagrid>
</List>
)
}
export default SongList