diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index 8a16027e..dbeb5189 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -185,6 +185,12 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { 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) start := time.Now() diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 96889c53..b8141e1a 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -12,6 +12,7 @@ type playlistTrackRepository struct { sqlRepository sqlRestful playlistId string + playlist *model.Playlist playlistRepo *playlistRepository } @@ -32,6 +33,7 @@ func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTrackReposi if pls.IsSmartPlaylist() { r.refreshSmartPlaylist(pls) } + p.playlist = pls return p } @@ -79,8 +81,12 @@ func (r *playlistTrackRepository) NewInstance() interface{} { 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) { - if !r.playlistRepo.isWritable(r.playlistId) { + if !r.isTracksEditable() { return 0, rest.ErrPermissionDenied } @@ -158,7 +164,7 @@ func (r *playlistTrackRepository) getTracks() ([]string, error) { } func (r *playlistTrackRepository) Delete(id string) error { - if !r.playlistRepo.isWritable(r.playlistId) { + if !r.isTracksEditable() { return rest.ErrPermissionDenied } 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 { - if !r.playlistRepo.isWritable(r.playlistId) { + if !r.isTracksEditable() { return rest.ErrPermissionDenied } ids, err := r.getTracks() diff --git a/server/nativeapi/playlists.go b/server/nativeapi/playlists.go index 11be6ddb..0685536a 100644 --- a/server/nativeapi/playlists.go +++ b/server/nativeapi/playlists.go @@ -176,6 +176,10 @@ func reorderItem(ds model.DataStore) http.HandlerFunc { } tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId) err = tracksRepo.Reorder(id, newPos) + if err == rest.ErrPermissionDenied { + http.Error(w, err.Error(), http.StatusForbidden) + return + } if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/ui/src/common/Writable.js b/ui/src/common/Writable.js index 0adedb1a..577bc1fc 100644 --- a/ui/src/common/Writable.js +++ b/ui/src/common/Writable.js @@ -20,3 +20,8 @@ export const Writable = (props) => { } return null } + +export const isSmartPlaylist = (pls) => !!pls.rules + +export const canChangeTracks = (pls) => + isWritable(pls.owner) && !isSmartPlaylist(pls) diff --git a/ui/src/layout/PlaylistsSubMenu.js b/ui/src/layout/PlaylistsSubMenu.js index 28c7c8d4..6cca07ea 100644 --- a/ui/src/layout/PlaylistsSubMenu.js +++ b/ui/src/layout/PlaylistsSubMenu.js @@ -12,7 +12,7 @@ import QueueMusicOutlinedIcon from '@material-ui/icons/QueueMusicOutlined' import { BiCog } from 'react-icons/all' import { useDrop } from 'react-dnd' import SubMenu from './SubMenu' -import { isWritable } from '../common' +import { canChangeTracks } from '../common' import { DraggableTypes, MAX_SIDEBAR_PLAYLISTS } from '../consts' const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => { @@ -20,7 +20,7 @@ const PlaylistMenuItemLink = ({ pls, sidebarIsOpen }) => { const notify = useNotify() const [, dropRef] = useDrop(() => ({ - accept: isWritable(pls.owner) ? DraggableTypes.ALL : [], + accept: canChangeTracks(pls) ? DraggableTypes.ALL : [], drop: (item) => dataProvider .addToPlaylist(pls.id, item) diff --git a/ui/src/playlist/PlaylistEdit.js b/ui/src/playlist/PlaylistEdit.js index affe1143..8f0b6029 100644 --- a/ui/src/playlist/PlaylistEdit.js +++ b/ui/src/playlist/PlaylistEdit.js @@ -10,7 +10,7 @@ import { required, useTranslate, } from 'react-admin' -import { Title } from '../common' +import { isSmartPlaylist, isWritable, Title } from '../common' const SyncFragment = ({ formData, variant, ...rest }) => { return ( @@ -27,16 +27,26 @@ const PlaylistTitle = ({ record }) => { return } -const PlaylistEdit = (props) => ( - <Edit title={<PlaylistTitle />} {...props}> - <SimpleForm redirect="list" variant={'outlined'}> +const PlaylistEditForm = (props) => { + const { record } = props + return ( + <SimpleForm redirect="list" variant={'outlined'} {...props}> <TextInput source="name" validate={required()} /> <TextInput multiline source="comment" /> - <BooleanInput source="public" /> + <BooleanInput + source="public" + disabled={!isWritable(record.owner) || isSmartPlaylist(record)} + /> <FormDataConsumer> {(formDataProps) => <SyncFragment {...formDataProps} />} </FormDataConsumer> </SimpleForm> + ) +} + +const PlaylistEdit = (props) => ( + <Edit title={<PlaylistTitle />} {...props}> + <PlaylistEditForm {...props} /> </Edit> ) diff --git a/ui/src/playlist/PlaylistList.js b/ui/src/playlist/PlaylistList.js index 10de7116..dcad5c5b 100644 --- a/ui/src/playlist/PlaylistList.js +++ b/ui/src/playlist/PlaylistList.js @@ -9,6 +9,7 @@ import { TextField, useUpdate, useNotify, + useRecordContext, } from 'react-admin' import Switch from '@material-ui/core/Switch' import { useMediaQuery } from '@material-ui/core' @@ -19,6 +20,7 @@ import { isWritable, useSelectedFields, useResourceRefresh, + isSmartPlaylist, } from '../common' import PlaylistListActions from './PlaylistListActions' @@ -28,7 +30,8 @@ const PlaylistFilter = (props) => ( </Filter> ) -const TogglePublicInput = ({ permissions, resource, record = {}, source }) => { +const TogglePublicInput = ({ resource, source }) => { + const record = useRecordContext() const notify = useNotify() const [togglePublic] = useUpdate( resource, @@ -51,20 +54,16 @@ const TogglePublicInput = ({ permissions, resource, record = {}, source }) => { e.stopPropagation() } - const canChange = - permissions === 'admin' || - localStorage.getItem('username') === record['owner'] - return ( <Switch checked={record[source]} 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 isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) useResourceRefresh('playlist') @@ -78,14 +77,10 @@ const PlaylistList = ({ permissions, ...props }) => { <DateField source="updatedAt" sortByOrder={'DESC'} /> ), public: !isXsmall && ( - <TogglePublicInput - source="public" - permissions={permissions} - sortByOrder={'DESC'} - /> + <TogglePublicInput source="public" sortByOrder={'DESC'} /> ), } - }, [isDesktop, isXsmall, permissions]) + }, [isDesktop, isXsmall]) const columns = useSelectedFields({ resource: 'playlist', diff --git a/ui/src/playlist/PlaylistShow.js b/ui/src/playlist/PlaylistShow.js index ab4f6009..66fb383a 100644 --- a/ui/src/playlist/PlaylistShow.js +++ b/ui/src/playlist/PlaylistShow.js @@ -10,7 +10,8 @@ import { makeStyles } from '@material-ui/core/styles' import PlaylistDetails from './PlaylistDetails' import PlaylistSongs from './PlaylistSongs' import PlaylistActions from './PlaylistActions' -import { Title, isReadOnly } from '../common' +import { Title, canChangeTracks } from '../common' + const useStyles = makeStyles( (theme) => ({ playlistActions: { @@ -42,7 +43,7 @@ const PlaylistShowLayout = (props) => { > <PlaylistSongs {...props} - readOnly={isReadOnly(record.owner)} + readOnly={!canChangeTracks(record)} title={<Title subTitle={record.name} />} actions={ <PlaylistActions diff --git a/ui/src/playlist/PlaylistSongs.js b/ui/src/playlist/PlaylistSongs.js index 108aac49..229d5b9b 100644 --- a/ui/src/playlist/PlaylistSongs.js +++ b/ui/src/playlist/PlaylistSongs.js @@ -182,6 +182,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => { <PlaylistSongBulkActions playlistId={playlistId} onUnselectItems={onUnselectItems} + readOnly={readOnly} /> </BulkActionsToolbar> <ReorderableList @@ -192,7 +193,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => { <SongDatagrid rowClick={(id) => dispatch(playTracks(data, ids, id))} {...listContext} - hasBulkActions={true} + hasBulkActions={!readOnly} contextAlwaysVisible={!isDesktop} classes={{ row: classes.row }} >