mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-18 07:53:19 +03:00
Add playlist views
This commit is contained in:
parent
b1f5d35f73
commit
8a709c489a
@ -77,15 +77,6 @@ func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playli
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTracksRepository {
|
|
||||||
p := &playlistTracksRepository{}
|
|
||||||
p.playlistId = playlistId
|
|
||||||
p.ctx = r.ctx
|
|
||||||
p.ormer = r.ormer
|
|
||||||
p.tableName = "playlist_tracks"
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *playlistRepository) updateTracks(id string, tracks model.MediaFiles) error {
|
func (r *playlistRepository) updateTracks(id string, tracks model.MediaFiles) error {
|
||||||
// Remove old tracks
|
// Remove old tracks
|
||||||
del := Delete("playlist_tracks").Where(Eq{"playlist_id": id})
|
del := Delete("playlist_tracks").Where(Eq{"playlist_id": id})
|
||||||
@ -94,10 +85,27 @@ func (r *playlistRepository) updateTracks(id string, tracks model.MediaFiles) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new tracks
|
// Break the track list in chunks to avoid hitting SQLITE_MAX_FUNCTION_ARG limit
|
||||||
for i, t := range tracks {
|
numTracks := len(tracks)
|
||||||
ins := Insert("playlist_tracks").Columns("playlist_id", "media_file_id", "id").
|
const chunkSize = 50
|
||||||
Values(id, t.ID, i)
|
var chunks [][]model.MediaFile
|
||||||
|
for i := 0; i < numTracks; i += chunkSize {
|
||||||
|
end := i + chunkSize
|
||||||
|
if end > numTracks {
|
||||||
|
end = numTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks = append(chunks, tracks[i:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new tracks, chunk by chunk
|
||||||
|
pos := 0
|
||||||
|
for i := range chunks {
|
||||||
|
ins := Insert("playlist_tracks").Columns("playlist_id", "media_file_id", "id")
|
||||||
|
for _, t := range chunks[i] {
|
||||||
|
ins = ins.Values(id, t.ID, pos)
|
||||||
|
pos++
|
||||||
|
}
|
||||||
_, err = r.executeSQL(ins)
|
_, err = r.executeSQL(ins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -159,5 +167,25 @@ func (r *playlistRepository) NewInstance() interface{} {
|
|||||||
return &model.Playlist{}
|
return &model.Playlist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) Save(entity interface{}) (string, error) {
|
||||||
|
pls := entity.(*model.Playlist)
|
||||||
|
pls.Owner = loggedUser(r.ctx).UserName
|
||||||
|
err := r.Put(pls)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return pls.ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) Update(entity interface{}, cols ...string) error {
|
||||||
|
pls := entity.(*model.Playlist)
|
||||||
|
err := r.Put(pls)
|
||||||
|
if err == model.ErrNotFound {
|
||||||
|
return rest.ErrNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var _ model.PlaylistRepository = (*playlistRepository)(nil)
|
var _ model.PlaylistRepository = (*playlistRepository)(nil)
|
||||||
var _ model.ResourceRepository = (*playlistRepository)(nil)
|
var _ rest.Repository = (*playlistRepository)(nil)
|
||||||
|
var _ rest.Persistable = (*playlistRepository)(nil)
|
||||||
|
@ -12,6 +12,18 @@ type playlistTracksRepository struct {
|
|||||||
playlistId string
|
playlistId string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTracksRepository {
|
||||||
|
p := &playlistTracksRepository{}
|
||||||
|
p.playlistId = playlistId
|
||||||
|
p.ctx = r.ctx
|
||||||
|
p.ormer = r.ormer
|
||||||
|
p.tableName = "playlist_tracks"
|
||||||
|
p.sortMappings = map[string]string{
|
||||||
|
"id": "playlist_tracks.id",
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (r *playlistTracksRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
func (r *playlistTracksRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
||||||
return r.count(Select().Where(Eq{"playlist_id": r.playlistId}), r.parseRestOptions(options...))
|
return r.count(Select().Where(Eq{"playlist_id": r.playlistId}), r.parseRestOptions(options...))
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ const App = () => (
|
|||||||
),
|
),
|
||||||
<Resource name="albumSong" />,
|
<Resource name="albumSong" />,
|
||||||
<Resource name="translation" />,
|
<Resource name="translation" />,
|
||||||
|
<Resource name="playlistTrack" />,
|
||||||
<Player />,
|
<Player />,
|
||||||
]}
|
]}
|
||||||
</Admin>
|
</Admin>
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { fetchUtils } from 'react-admin'
|
import { fetchUtils } from 'react-admin'
|
||||||
import jsonServerProvider from 'ra-data-json-server'
|
import baseUrl from '../utils/baseUrl'
|
||||||
import baseUrl from './utils/baseUrl'
|
import config from '../config'
|
||||||
import config from './config'
|
|
||||||
|
|
||||||
const restUrl = '/app/api'
|
|
||||||
const customAuthorizationHeader = 'X-ND-Authorization'
|
const customAuthorizationHeader = 'X-ND-Authorization'
|
||||||
|
|
||||||
const httpClient = (url, options = {}) => {
|
const httpClient = (url, options = {}) => {
|
||||||
url = baseUrl(url)
|
url = baseUrl(url)
|
||||||
url = url.replace(restUrl + '/albumSong', restUrl + '/song')
|
|
||||||
if (!options.headers) {
|
if (!options.headers) {
|
||||||
options.headers = new Headers({ Accept: 'application/json' })
|
options.headers = new Headers({ Accept: 'application/json' })
|
||||||
}
|
}
|
||||||
@ -27,6 +24,4 @@ const httpClient = (url, options = {}) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider = jsonServerProvider(restUrl, httpClient)
|
export default httpClient
|
||||||
|
|
||||||
export default dataProvider
|
|
3
ui/src/dataProvider/index.js
Normal file
3
ui/src/dataProvider/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import wrapperDataProvider from './wrapperDataProvider'
|
||||||
|
|
||||||
|
export default wrapperDataProvider
|
68
ui/src/dataProvider/wrapperDataProvider.js
Normal file
68
ui/src/dataProvider/wrapperDataProvider.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import jsonServerProvider from 'ra-data-json-server'
|
||||||
|
import httpClient from './httpClient'
|
||||||
|
|
||||||
|
const restUrl = '/app/api'
|
||||||
|
|
||||||
|
const dataProvider = jsonServerProvider(restUrl, httpClient)
|
||||||
|
|
||||||
|
const mapResource = (resource, params) => {
|
||||||
|
console.log('R: ', resource, 'P: ', params)
|
||||||
|
switch (resource) {
|
||||||
|
case 'albumSong':
|
||||||
|
return ['song', params]
|
||||||
|
|
||||||
|
case 'playlistTrack':
|
||||||
|
// /api/playlistTrack?playlist_id=123 => /api/playlist/123/tracks
|
||||||
|
let plsId = '0'
|
||||||
|
if (params.filter) {
|
||||||
|
plsId = params.filter.playlist_id
|
||||||
|
delete params.filter.playlist_id
|
||||||
|
}
|
||||||
|
return [`playlist/${plsId}/tracks`, params]
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [resource, params]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapperDataProvider = {
|
||||||
|
...dataProvider,
|
||||||
|
getList: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.getList(r, p)
|
||||||
|
},
|
||||||
|
getOne: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.getOne(r, p)
|
||||||
|
},
|
||||||
|
getMany: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.getMany(r, p)
|
||||||
|
},
|
||||||
|
getManyReference: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.getManyReference(r, p)
|
||||||
|
},
|
||||||
|
update: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.update(r, p)
|
||||||
|
},
|
||||||
|
updateMany: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.updateMany(r, p)
|
||||||
|
},
|
||||||
|
create: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.create(r, p)
|
||||||
|
},
|
||||||
|
delete: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.delete(r, p)
|
||||||
|
},
|
||||||
|
deleteMany: (resource, params) => {
|
||||||
|
const [r, p] = mapResource(resource, params)
|
||||||
|
return dataProvider.deleteMany(r, p)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wrapperDataProvider
|
20
ui/src/playlist/PlaylistCreate.js
Normal file
20
ui/src/playlist/PlaylistCreate.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Create,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
BooleanInput,
|
||||||
|
required,
|
||||||
|
} from 'react-admin'
|
||||||
|
|
||||||
|
const PlaylistCreate = (props) => (
|
||||||
|
<Create {...props}>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="name" validate={required()} />
|
||||||
|
<TextInput multiline source="comment" />
|
||||||
|
<BooleanInput source="public" initialValue={true} />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PlaylistCreate
|
20
ui/src/playlist/PlaylistEdit.js
Normal file
20
ui/src/playlist/PlaylistEdit.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
BooleanInput,
|
||||||
|
required,
|
||||||
|
} from 'react-admin'
|
||||||
|
|
||||||
|
const PlaylistEdit = (props) => (
|
||||||
|
<Edit {...props}>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="name" validate={required()} />
|
||||||
|
<TextInput source="comment" />
|
||||||
|
<BooleanInput source="public" />
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PlaylistEdit
|
@ -1,22 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
List,
|
|
||||||
Datagrid,
|
Datagrid,
|
||||||
TextField,
|
TextField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
NumberField,
|
NumberField,
|
||||||
DateField,
|
DateField,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { DurationField, Title } from '../common'
|
import { DurationField, List } from '../common'
|
||||||
|
|
||||||
const PlaylistList = (props) => (
|
const PlaylistList = (props) => (
|
||||||
<List
|
<List {...props} exporter={false}>
|
||||||
{...props}
|
|
||||||
title={
|
|
||||||
<Title subTitle={'resources.playlist.name'} args={{ smart_count: 2 }} />
|
|
||||||
}
|
|
||||||
exporter={false}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="edit">
|
<Datagrid rowClick="edit">
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<TextField source="owner" />
|
<TextField source="owner" />
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import PlaylistIcon from '../icons/Playlist'
|
import PlaylistIcon from '../icons/Playlist'
|
||||||
import PlaylistList from './PlaylistList'
|
import PlaylistList from './PlaylistList'
|
||||||
|
import PlaylistEdit from './PlaylistEdit'
|
||||||
|
import PlaylistCreate from './PlaylistCreate'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
icon: PlaylistIcon,
|
icon: PlaylistIcon,
|
||||||
list: PlaylistList,
|
list: PlaylistList,
|
||||||
|
create: PlaylistCreate,
|
||||||
|
edit: PlaylistEdit,
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user