Add export as m3u button to playlist
This commit is contained in:
@@ -116,7 +116,8 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectPlaylist": "Selecione a playlist:",
|
"selectPlaylist": "Selecione a playlist:",
|
||||||
"addNewPlaylist": "Criar \"%{name}\""
|
"addNewPlaylist": "Criar \"%{name}\"",
|
||||||
|
"export": "Exportar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-2
@@ -56,16 +56,24 @@ func handleExportPlaylist(ds model.DataStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug(ctx, "Exporting playlist as M3U", "playlistId", plsId, "name", pls.Name)
|
||||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||||
|
disposition := fmt.Sprintf("attachment; filename=\"%s.m3u\"", pls.Name)
|
||||||
|
w.Header().Set("Content-Disposition", disposition)
|
||||||
|
|
||||||
// TODO: Move this and the import playlist logic to `core`
|
// TODO: Move this and the import playlist logic to `core`
|
||||||
w.Write([]byte("#EXTM3U\n"))
|
_, err = w.Write([]byte("#EXTM3U\n"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Error sending playlist", "name", pls.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, t := range pls.Tracks {
|
for _, t := range pls.Tracks {
|
||||||
header := fmt.Sprintf("#EXTINF:%.f,%s - %s\n", t.Duration, t.Artist, t.Title)
|
header := fmt.Sprintf("#EXTINF:%.f,%s - %s\n", t.Duration, t.Artist, t.Title)
|
||||||
line := t.Path + "\n"
|
line := t.Path + "\n"
|
||||||
_, err := w.Write([]byte(header + line))
|
_, err = w.Write([]byte(header + line))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error sending playlist", "name", pls.Name)
|
log.Error(ctx, "Error sending playlist", "name", pls.Name)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const REST_URL = '/app/api'
|
||||||
|
|
||||||
|
export const M3U_MIME_TYPE = 'audio/x-mpegurl'
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import httpClient from './httpClient'
|
||||||
import wrapperDataProvider from './wrapperDataProvider'
|
import wrapperDataProvider from './wrapperDataProvider'
|
||||||
|
|
||||||
|
export { httpClient }
|
||||||
|
|
||||||
export default wrapperDataProvider
|
export default wrapperDataProvider
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import jsonServerProvider from 'ra-data-json-server'
|
import jsonServerProvider from 'ra-data-json-server'
|
||||||
import httpClient from './httpClient'
|
import httpClient from './httpClient'
|
||||||
|
import { REST_URL } from '../consts'
|
||||||
|
|
||||||
const restUrl = '/app/api'
|
const dataProvider = jsonServerProvider(REST_URL, httpClient)
|
||||||
|
|
||||||
const dataProvider = jsonServerProvider(restUrl, httpClient)
|
|
||||||
|
|
||||||
const mapResource = (resource, params) => {
|
const mapResource = (resource, params) => {
|
||||||
switch (resource) {
|
switch (resource) {
|
||||||
|
|||||||
+2
-1
@@ -85,7 +85,8 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectPlaylist": "Select a playlist:",
|
"selectPlaylist": "Select a playlist:",
|
||||||
"addNewPlaylist": "Create \"%{name}\""
|
"addNewPlaylist": "Create \"%{name}\"",
|
||||||
|
"export": "Export"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow'
|
import PlayArrowIcon from '@material-ui/icons/PlayArrow'
|
||||||
import ShuffleIcon from '@material-ui/icons/Shuffle'
|
import ShuffleIcon from '@material-ui/icons/Shuffle'
|
||||||
import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined'
|
import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined'
|
||||||
|
import QueueMusicIcon from '@material-ui/icons/QueueMusic'
|
||||||
|
import { httpClient } from '../dataProvider'
|
||||||
import { playTracks, shuffleTracks } from '../audioplayer'
|
import { playTracks, shuffleTracks } from '../audioplayer'
|
||||||
|
import { M3U_MIME_TYPE, REST_URL } from '../consts'
|
||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
|
|
||||||
const PlaylistActions = ({
|
const PlaylistActions = ({
|
||||||
@@ -18,38 +21,68 @@ const PlaylistActions = ({
|
|||||||
data,
|
data,
|
||||||
exporter,
|
exporter,
|
||||||
permanentFilter,
|
permanentFilter,
|
||||||
playlistId,
|
record,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const translate = useTranslate()
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
const handlePlay = React.useCallback(() => {
|
||||||
|
dispatch(playTracks(data, ids))
|
||||||
|
}, [dispatch, data, ids])
|
||||||
|
|
||||||
|
const handleShuffle = React.useCallback(() => {
|
||||||
|
dispatch(shuffleTracks(data, ids))
|
||||||
|
}, [dispatch, data, ids])
|
||||||
|
|
||||||
|
const handleDownload = React.useCallback(() => {
|
||||||
|
subsonic.download(record.id)
|
||||||
|
}, [record])
|
||||||
|
|
||||||
|
const handleExport = React.useCallback(
|
||||||
|
() =>
|
||||||
|
httpClient(`${REST_URL}/playlist/${record.id}/tracks`, {
|
||||||
|
headers: new Headers({ Accept: M3U_MIME_TYPE }),
|
||||||
|
}).then((res) => {
|
||||||
|
console.log(res)
|
||||||
|
const blob = new Blob([res.body], { type: M3U_MIME_TYPE })
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${record.name}.m3u`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.parentNode.removeChild(link)
|
||||||
|
}),
|
||||||
|
[record]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={handlePlay}
|
||||||
dispatch(playTracks(data, ids))
|
|
||||||
}}
|
|
||||||
label={translate('resources.album.actions.playAll')}
|
label={translate('resources.album.actions.playAll')}
|
||||||
>
|
>
|
||||||
<PlayArrowIcon />
|
<PlayArrowIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={handleShuffle}
|
||||||
dispatch(shuffleTracks(data, ids))
|
|
||||||
}}
|
|
||||||
label={translate('resources.album.actions.shuffle')}
|
label={translate('resources.album.actions.shuffle')}
|
||||||
>
|
>
|
||||||
<ShuffleIcon />
|
<ShuffleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={handleDownload}
|
||||||
subsonic.download(playlistId)
|
|
||||||
}}
|
|
||||||
label={translate('resources.album.actions.download')}
|
label={translate('resources.album.actions.download')}
|
||||||
>
|
>
|
||||||
<CloudDownloadOutlinedIcon />
|
<CloudDownloadOutlinedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleExport}
|
||||||
|
label={translate('resources.playlist.actions.export')}
|
||||||
|
>
|
||||||
|
<QueueMusicIcon />
|
||||||
|
</Button>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const PlaylistShow = (props) => {
|
|||||||
playlistId={props.id}
|
playlistId={props.id}
|
||||||
readOnly={isReadOnly(record && record.owner)}
|
readOnly={isReadOnly(record && record.owner)}
|
||||||
title={<Title subTitle={record && record.name} />}
|
title={<Title subTitle={record && record.name} />}
|
||||||
actions={<PlaylistActions playlistId={props.id} />}
|
actions={<PlaylistActions record={record} />}
|
||||||
filter={{ playlist_id: props.id }}
|
filter={{ playlist_id: props.id }}
|
||||||
resource={'playlistTrack'}
|
resource={'playlistTrack'}
|
||||||
exporter={false}
|
exporter={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user