mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-02 08:31:27 +03:00
Make SmartPlaylists read-only
This commit is contained in:
parent
d169f54e7d
commit
2e2a647e67
@ -185,6 +185,12 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Never refresh other users' playlists
|
||||||
|
usr := loggedUser(r.ctx)
|
||||||
|
if pls.Owner != usr.UserName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug(r.ctx, "Refreshing smart playlist", "playlist", pls.Name, "id", pls.ID)
|
log.Debug(r.ctx, "Refreshing smart playlist", "playlist", pls.Name, "id", pls.ID)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ type playlistTrackRepository struct {
|
|||||||
sqlRepository
|
sqlRepository
|
||||||
sqlRestful
|
sqlRestful
|
||||||
playlistId string
|
playlistId string
|
||||||
|
playlist *model.Playlist
|
||||||
playlistRepo *playlistRepository
|
playlistRepo *playlistRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTrackReposi
|
|||||||
if pls.IsSmartPlaylist() {
|
if pls.IsSmartPlaylist() {
|
||||||
r.refreshSmartPlaylist(pls)
|
r.refreshSmartPlaylist(pls)
|
||||||
}
|
}
|
||||||
|
p.playlist = pls
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +81,12 @@ func (r *playlistTrackRepository) NewInstance() interface{} {
|
|||||||
return &model.PlaylistTrack{}
|
return &model.PlaylistTrack{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playlistTrackRepository) isTracksEditable() bool {
|
||||||
|
return r.playlistRepo.isWritable(r.playlistId) && !r.playlist.IsSmartPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *playlistTrackRepository) Add(mediaFileIds []string) (int, error) {
|
func (r *playlistTrackRepository) Add(mediaFileIds []string) (int, error) {
|
||||||
if !r.playlistRepo.isWritable(r.playlistId) {
|
if !r.isTracksEditable() {
|
||||||
return 0, rest.ErrPermissionDenied
|
return 0, rest.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +164,7 @@ func (r *playlistTrackRepository) getTracks() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *playlistTrackRepository) Delete(id string) error {
|
func (r *playlistTrackRepository) Delete(id string) error {
|
||||||
if !r.playlistRepo.isWritable(r.playlistId) {
|
if !r.isTracksEditable() {
|
||||||
return rest.ErrPermissionDenied
|
return rest.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
err := r.delete(And{Eq{"playlist_id": r.playlistId}, Eq{"id": id}})
|
err := r.delete(And{Eq{"playlist_id": r.playlistId}, Eq{"id": id}})
|
||||||
@ -172,7 +178,7 @@ func (r *playlistTrackRepository) Delete(id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *playlistTrackRepository) Reorder(pos int, newPos int) error {
|
func (r *playlistTrackRepository) Reorder(pos int, newPos int) error {
|
||||||
if !r.playlistRepo.isWritable(r.playlistId) {
|
if !r.isTracksEditable() {
|
||||||
return rest.ErrPermissionDenied
|
return rest.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
ids, err := r.getTracks()
|
ids, err := r.getTracks()
|
||||||
|
@ -176,6 +176,10 @@ func reorderItem(ds model.DataStore) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId)
|
tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId)
|
||||||
err = tracksRepo.Reorder(id, newPos)
|
err = tracksRepo.Reorder(id, newPos)
|
||||||
|
if err == rest.ErrPermissionDenied {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -20,3 +20,8 @@ export const Writable = (props) => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isSmartPlaylist = (pls) => !!pls.rules
|
||||||
|
|
||||||
|
export const canChangeTracks = (pls) =>
|
||||||
|
isWritable(pls.owner) && !isSmartPlaylist(pls)
|
||||||
|
@ -12,7 +12,7 @@ import QueueMusicOutlinedIcon from '@material-ui/icons/QueueMusicOutlined'
|
|||||||
import { BiCog } from 'react-icons/all'
|
import { BiCog } from 'react-icons/all'
|
||||||
import { useDrop } from 'react-dnd'
|
import { useDrop } from 'react-dnd'
|
||||||
import SubMenu from './SubMenu'
|
import SubMenu from './SubMenu'
|
||||||
import { isWritable } from '../common'
|
import { canChangeTracks } from '../common'
|
||||||
import { DraggableTypes, MAX_SIDEBAR_PLAYLISTS } from '../consts'
|
import { DraggableTypes, MAX_SIDEBAR_PLAYLISTS } from '../consts'
|
||||||
|
|
||||||
const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => {
|
const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => {
|
||||||
@ -20,7 +20,7 @@ const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => {
|
|||||||
const notify = useNotify()
|
const notify = useNotify()
|
||||||
|
|
||||||
const [, dropRef] = useDrop(() => ({
|
const [, dropRef] = useDrop(() => ({
|
||||||
accept: isWritable(pls.owner) ? DraggableTypes.ALL : [],
|
accept: canChangeTracks(pls) ? DraggableTypes.ALL : [],
|
||||||
drop: (item) =>
|
drop: (item) =>
|
||||||
dataProvider
|
dataProvider
|
||||||
.addToPlaylist(pls.id, item)
|
.addToPlaylist(pls.id, item)
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
required,
|
required,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { Title } from '../common'
|
import { isSmartPlaylist, isWritable, Title } from '../common'
|
||||||
|
|
||||||
const SyncFragment = ({ formData, variant, ...rest }) => {
|
const SyncFragment = ({ formData, variant, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
@ -27,16 +27,26 @@ const PlaylistTitle = ({ record }) => {
|
|||||||
return <Title subTitle={`${resourceName} "${record ? record.name : ''}"`} />
|
return <Title subTitle={`${resourceName} "${record ? record.name : ''}"`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistEdit = (props) => (
|
const PlaylistEditForm = (props) => {
|
||||||
<Edit title={<PlaylistTitle />} {...props}>
|
const { record } = props
|
||||||
<SimpleForm redirect="list" variant={'outlined'}>
|
return (
|
||||||
|
<SimpleForm redirect="list" variant={'outlined'} {...props}>
|
||||||
<TextInput source="name" validate={required()} />
|
<TextInput source="name" validate={required()} />
|
||||||
<TextInput multiline source="comment" />
|
<TextInput multiline source="comment" />
|
||||||
<BooleanInput source="public" />
|
<BooleanInput
|
||||||
|
source="public"
|
||||||
|
disabled={!isWritable(record.owner) || isSmartPlaylist(record)}
|
||||||
|
/>
|
||||||
<FormDataConsumer>
|
<FormDataConsumer>
|
||||||
{(formDataProps) => <SyncFragment {...formDataProps} />}
|
{(formDataProps) => <SyncFragment {...formDataProps} />}
|
||||||
</FormDataConsumer>
|
</FormDataConsumer>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaylistEdit = (props) => (
|
||||||
|
<Edit title={<PlaylistTitle />} {...props}>
|
||||||
|
<PlaylistEditForm {...props} />
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
useUpdate,
|
useUpdate,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
useRecordContext,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import Switch from '@material-ui/core/Switch'
|
import Switch from '@material-ui/core/Switch'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
isWritable,
|
isWritable,
|
||||||
useSelectedFields,
|
useSelectedFields,
|
||||||
useResourceRefresh,
|
useResourceRefresh,
|
||||||
|
isSmartPlaylist,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import PlaylistListActions from './PlaylistListActions'
|
import PlaylistListActions from './PlaylistListActions'
|
||||||
|
|
||||||
@ -28,7 +30,8 @@ const PlaylistFilter = (props) => (
|
|||||||
</Filter>
|
</Filter>
|
||||||
)
|
)
|
||||||
|
|
||||||
const TogglePublicInput = ({ permissions, resource, record = {}, source }) => {
|
const TogglePublicInput = ({ resource, source }) => {
|
||||||
|
const record = useRecordContext()
|
||||||
const notify = useNotify()
|
const notify = useNotify()
|
||||||
const [togglePublic] = useUpdate(
|
const [togglePublic] = useUpdate(
|
||||||
resource,
|
resource,
|
||||||
@ -51,20 +54,16 @@ const TogglePublicInput = ({ permissions, resource, record = {}, source }) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
const canChange =
|
|
||||||
permissions === 'admin' ||
|
|
||||||
localStorage.getItem('username') === record['owner']
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={record[source]}
|
checked={record[source]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={!canChange}
|
disabled={!isWritable(record.owner) || isSmartPlaylist(record)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistList = ({ permissions, ...props }) => {
|
const PlaylistList = (props) => {
|
||||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
useResourceRefresh('playlist')
|
useResourceRefresh('playlist')
|
||||||
@ -78,14 +77,10 @@ const PlaylistList = ({ permissions, ...props }) => {
|
|||||||
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
||||||
),
|
),
|
||||||
public: !isXsmall && (
|
public: !isXsmall && (
|
||||||
<TogglePublicInput
|
<TogglePublicInput source="public" sortByOrder={'DESC'} />
|
||||||
source="public"
|
|
||||||
permissions={permissions}
|
|
||||||
sortByOrder={'DESC'}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}, [isDesktop, isXsmall, permissions])
|
}, [isDesktop, isXsmall])
|
||||||
|
|
||||||
const columns = useSelectedFields({
|
const columns = useSelectedFields({
|
||||||
resource: 'playlist',
|
resource: 'playlist',
|
||||||
|
@ -10,7 +10,8 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import PlaylistDetails from './PlaylistDetails'
|
import PlaylistDetails from './PlaylistDetails'
|
||||||
import PlaylistSongs from './PlaylistSongs'
|
import PlaylistSongs from './PlaylistSongs'
|
||||||
import PlaylistActions from './PlaylistActions'
|
import PlaylistActions from './PlaylistActions'
|
||||||
import { Title, isReadOnly } from '../common'
|
import { Title, canChangeTracks } from '../common'
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
(theme) => ({
|
(theme) => ({
|
||||||
playlistActions: {
|
playlistActions: {
|
||||||
@ -42,7 +43,7 @@ const PlaylistShowLayout = (props) => {
|
|||||||
>
|
>
|
||||||
<PlaylistSongs
|
<PlaylistSongs
|
||||||
{...props}
|
{...props}
|
||||||
readOnly={isReadOnly(record.owner)}
|
readOnly={!canChangeTracks(record)}
|
||||||
title={<Title subTitle={record.name} />}
|
title={<Title subTitle={record.name} />}
|
||||||
actions={
|
actions={
|
||||||
<PlaylistActions
|
<PlaylistActions
|
||||||
|
@ -182,6 +182,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
|
|||||||
<PlaylistSongBulkActions
|
<PlaylistSongBulkActions
|
||||||
playlistId={playlistId}
|
playlistId={playlistId}
|
||||||
onUnselectItems={onUnselectItems}
|
onUnselectItems={onUnselectItems}
|
||||||
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</BulkActionsToolbar>
|
</BulkActionsToolbar>
|
||||||
<ReorderableList
|
<ReorderableList
|
||||||
@ -192,7 +193,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
|
|||||||
<SongDatagrid
|
<SongDatagrid
|
||||||
rowClick={(id) => dispatch(playTracks(data, ids, id))}
|
rowClick={(id) => dispatch(playTracks(data, ids, id))}
|
||||||
{...listContext}
|
{...listContext}
|
||||||
hasBulkActions={true}
|
hasBulkActions={!readOnly}
|
||||||
contextAlwaysVisible={!isDesktop}
|
contextAlwaysVisible={!isDesktop}
|
||||||
classes={{ row: classes.row }}
|
classes={{ row: classes.row }}
|
||||||
>
|
>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user