[Jellyfin UI] Implemented add to/remove from playlist.

Closes: #414
This commit is contained in:
Fabio Manganiello 2024-11-10 00:29:24 +01:00
parent 2df88c1911
commit d799d50391
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 153 additions and 45 deletions

View file

@ -5,19 +5,30 @@
Playlist name Playlist name
</TextPrompt> </TextPrompt>
<div class="playlists"> <div class="playlists-container">
<div class="playlist new-playlist"> <div class="header">
<button @click="showNewPlaylist = true"> <div class="filter">
<i class="fa fa-plus" /> <input type="text"
Create new playlist placeholder="Filter playlists"
</button> ref="playlistFilter"
@input="filter = $event.target.value">
</div>
<div class="playlist new-playlist">
<button @click="showNewPlaylist = true">
<i class="fa fa-plus" />
Create new playlist
</button>
</div>
</div> </div>
<div class="playlist" v-for="playlist in playlists" :key="playlist.id"> <div class="playlists">
<button @click="addToPlaylist(playlist.id)"> <div class="playlist" v-for="playlist in sortedPlaylists" :key="playlist.id">
<i class="fa fa-list" /> <button @click="addToPlaylist(playlist.id)">
{{ playlist.name }} <i class="fa fa-list" />
</button> {{ playlist.name }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -41,13 +52,53 @@ export default {
data() { data() {
return { return {
filter: '',
loading: false, loading: false,
playlists: [], playlists: [],
showNewPlaylist: false, showNewPlaylist: false,
} }
}, },
computed: {
pluginName() {
switch (this.item.type) {
case 'youtube':
return 'youtube'
case 'jellyfin':
return 'media.jellyfin'
default:
return null
}
},
sortedPlaylists() {
return this.playlists
.filter((playlist) => playlist.name.toLowerCase().includes(this.filter.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name))
},
},
methods: { methods: {
checkPlugin() {
if (!this.pluginName) {
this.notify({
title: 'Unsupported item type',
text: `Item type ${this.item.type} does not support playlists`,
warning: true,
image: {
icon: 'exclamation-triangle',
}
})
console.warn(`Unsupported item type: ${this.item.type}`)
return false
}
return true
},
async createPlaylist(name) { async createPlaylist(name) {
name = name?.trim() name = name?.trim()
if (!name?.length) if (!name?.length)
@ -56,23 +107,11 @@ export default {
this.loading = true this.loading = true
try { try {
const playlist = await this.request('youtube.create_playlist', { const playlist = await this.request(`${this.pluginName}.create_playlist`, {
name: name, name: name,
}) })
await this.request('youtube.add_to_playlist', { await this.addToPlaylist(playlist.id)
playlist_id: playlist.id,
video_id: this.item.id || this.item.url,
})
this.$emit('done')
this.notify({
text: 'Playlist created and video added',
image: {
icon: 'check',
}
})
} finally { } finally {
this.loading = false this.loading = false
this.showNewPlaylist = false this.showNewPlaylist = false
@ -80,26 +119,30 @@ export default {
}, },
async refreshPlaylists() { async refreshPlaylists() {
this.loading = true if (!this.checkPlugin())
return
this.loading = true
try { try {
this.playlists = await this.request('youtube.get_playlists') this.playlists = await this.request(`${this.pluginName}.get_playlists`)
} finally { } finally {
this.loading = false this.loading = false
} }
}, },
async addToPlaylist(playlistId) { async addToPlaylist(playlistId) {
this.loading = true if (!this.checkPlugin())
return
this.loading = true
try { try {
await this.request('youtube.add_to_playlist', { await this.request(`${this.pluginName}.add_to_playlist`, {
playlist_id: playlistId, playlist_id: playlistId,
video_id: this.item.id || this.item.url, item_ids: [this.item.id || this.item.url],
}) })
this.notify({ this.notify({
text: 'Video added to playlist', text: 'Item added to playlist',
image: { image: {
icon: 'check', icon: 'check',
} }
@ -112,6 +155,15 @@ export default {
}, },
}, },
watch: {
loading() {
if (this.loading)
return
this.$nextTick(() => this.$refs.playlistFilter.focus())
},
},
mounted() { mounted() {
this.refreshPlaylists() this.refreshPlaylists()
}, },
@ -119,19 +171,47 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$header-height: 6.5em;
.playlist-adder-container { .playlist-adder-container {
min-width: 300px; width: 30em;
height: 100%; height: fit-content;
max-width: 90vw;
max-height: 70vh;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.playlists-container {
width: 100%;
height: 100%;
.header {
width: 100%;
height: $header-height;
display: flex;
flex-direction: column;
}
}
.playlists { .playlists {
width: 100%; width: 100%;
height: calc(100% - #{$header-height});
overflow-y: auto; overflow-y: auto;
} }
.filter {
width: 100%;
display: flex;
input[type="text"] {
width: 100%;
margin: 0.5em 0.5em 0.25em 0.5em;
padding: 0.5em;
}
}
.playlist { .playlist {
button { button {
width: 100%; width: 100%;

View file

@ -15,6 +15,7 @@
<Media v-bind="componentData.props" <Media v-bind="componentData.props"
v-on="componentData.on" v-on="componentData.on"
:collection="collection" :collection="collection"
@add-to-playlist="$emit('add-to-playlist', $event)"
@delete="deleteItem" @delete="deleteItem"
@select="select" @select="select"
@select-collection="selectCollection" @select-collection="selectCollection"

View file

@ -131,8 +131,8 @@ export default {
try { try {
await this.request('media.jellyfin.playlist_move', { await this.request('media.jellyfin.playlist_move', {
playlist: this.collection.id, playlist_id: this.collection.id,
playlist_item_id: item.playlist_item_id, item_id: item.playlist_item_id,
to_pos: to, to_pos: to,
}) })

View file

@ -92,7 +92,7 @@
@move="$emit('playlist-move', $event)" @move="$emit('playlist-move', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-with-opts="$emit('play-with-opts', $event)" @play-with-opts="$emit('play-with-opts', $event)"
@remove-from-playlist="$emit('remove-from-playlist', $event)" @remove-from-playlist="removeFromPlaylist"
@select="selectedResult = $event" @select="selectedResult = $event"
@view="$emit('view', $event)" @view="$emit('view', $event)"
v-if="mediaItems?.length > 0" /> v-if="mediaItems?.length > 0" />
@ -131,7 +131,6 @@ export default {
'play', 'play',
'play-with-opts', 'play-with-opts',
'playlist-move', 'playlist-move',
'remove-from-playlist',
'select', 'select',
'select-collection', 'select-collection',
'view', 'view',
@ -178,7 +177,11 @@ export default {
mediaItems() { mediaItems() {
return ( return (
this.sortedItems?.filter((item) => !['collection', 'artist', 'album'].includes(item.item_type)) ?? [] this.sortedItems?.filter((item) => !['collection', 'artist', 'album'].includes(item.item_type)) ?? []
).sort((a, b) => { ).map((item) => {
item.media_type = item.type
item.type = 'jellyfin'
return item
}).sort((a, b) => {
if (this.view === 'playlist') { if (this.view === 'playlist') {
// Skip sorting if this is a playlist // Skip sorting if this is a playlist
return 0 return 0
@ -219,6 +222,20 @@ export default {
}, },
methods: { methods: {
async removeFromPlaylist(item) {
this.loading_ = true
try {
await this.request('media.jellyfin.remove_from_playlist', {
playlist_id: this.collection.id,
item_ids: [item.playlist_item_id],
})
await this.refresh()
} finally {
this.loading_ = false
}
},
async selectArtist() { async selectArtist() {
const artistId = this.displayedArtist?.id || this.getUrlArgs().artist const artistId = this.displayedArtist?.id || this.getUrlArgs().artist
if (!artistId?.length) if (!artistId?.length)
@ -319,12 +336,22 @@ export default {
break break
case 'playlist': case 'playlist':
this.items = await this.request( this.items = this.collection?.item_type === 'playlist' ? (
'media.jellyfin.get_playlist_items', await this.request(
{ 'media.jellyfin.get_playlist_items',
playlist: this.collection.id, {
limit: 25000, playlist_id: this.collection.id,
} limit: 25000,
}
)
) : (
await this.request(
'media.jellyfin.get_items',
{
parent_id: this.collection.id,
limit: 25000,
}
)
) )
break break

View file

@ -133,7 +133,7 @@ export default {
try { try {
await this.request('youtube.remove_from_playlist', { await this.request('youtube.remove_from_playlist', {
playlist_id: playlistId, playlist_id: playlistId,
video_id: videoId, item_ids: [videoId],
}) })
} finally { } finally {
this.loading_ = false this.loading_ = false