diff --git a/src/app/modules/api/playqueue/playqueue-auxapp.model.ts b/src/app/modules/api/playqueue/playqueue-auxapp.model.ts new file mode 100644 index 00000000..e79631fe --- /dev/null +++ b/src/app/modules/api/playqueue/playqueue-auxapp.model.ts @@ -0,0 +1,51 @@ +import {AuxappModel} from '../auxapp/auxapp.model'; +import {nested} from '../../backbone/decorators/nested.decorator'; +import {attributesKey} from '../../backbone/decorators/attributes-key.decorator'; +import {PlayqueueItemsAuxappCollection} from './playqueue-item/playqueue-items-auxapp.collection'; +import {PlayqueueItemAuxappModel} from './playqueue-item/playqueue-item-auxapp.model'; + +export class PlayqueueAuxappModel extends AuxappModel { + endpoint = '/queue'; + + @attributesKey('items') + @nested() + items: PlayqueueItemsAuxappCollection; + + parse(attributes) { + delete attributes.items; + return attributes; + } + + fetch(...args) { + const id = this.id; + this.set('id', 'mine'); + const superCall = super.fetch.apply(this, ...args).then(() => { + this.items.fetch(); + return this; + }); + this.set('id', id); + return superCall; + } + + save() { + if (this.id) { + let index = 0; + this.items.each((item) => { + if (index < 3) { + item.save(); + } + index++; + }); + } + } + + initialize() { + if (this.id) { + this.items.setEndpoint(this.id); + } + this.on('change:id', () => { + this.items.setEndpoint(this.id); + }); + } +} + diff --git a/src/app/modules/api/playqueue/playqueue-item/playqueue-item-auxapp.model.ts b/src/app/modules/api/playqueue/playqueue-item/playqueue-item-auxapp.model.ts new file mode 100644 index 00000000..b48e8b75 --- /dev/null +++ b/src/app/modules/api/playqueue/playqueue-item/playqueue-item-auxapp.model.ts @@ -0,0 +1,152 @@ +import {PlaylistItemAuxappModel} from '../../playlists/playlist-item/playlist-item-auxapp.model'; +import {isNumber} from 'underscore'; +import {PlayQueueItemStatus} from '../../../player/src/playqueue-item-status.enum'; +import {dynamicInstance} from '../../../backbone/decorators/dynamic-instance.decorator'; +import {ITrack} from '../../tracks/track.interface'; +import {TrackMixcloudModel} from '../../tracks/track-mixcloud.model'; +import {attributesKey} from '../../../backbone/decorators/attributes-key.decorator'; +import {TrackSoundcloudModel} from '../../tracks/track-soundcloud.model'; +import {defaultValue} from '../../../backbone/decorators/default-value.decorator'; +import {TrackYoutubeModel} from '../../tracks/track-youtube.model'; +import {PlayqueueItemsAuxappCollection} from './playqueue-items-auxapp.collection'; + +export class PlayqueueItemAuxappModel + extends PlaylistItemAuxappModel { + + private _promisePerState = {}; + + @attributesKey('state') + @defaultValue(PlayQueueItemStatus.Scheduled) + status: PlayQueueItemStatus; + + @attributesKey('track') + @dynamicInstance({ + identifierKey: 'provider', + identifierKeyValueMap: { + 'soundcloud': TrackSoundcloudModel, + 'youtube': TrackYoutubeModel, + 'mixcloud': TrackMixcloudModel + } + }) + track: ITrack; + + @attributesKey('progress') + @defaultValue(0) + progress: number; + + @attributesKey('duration') + @defaultValue(0) + duration: number; + + @attributesKey('indexBeforeShuffle') + indexBeforeShuffle: number; + + idAttribute = 'ABC'; + + url = () => { + return (>this.collection).url(); + }; + + private resolveOnStatus(requestedStatus): Promise { + if (!this._promisePerState[requestedStatus]) { + this._promisePerState[requestedStatus] = new Promise(resolve => { + if (this.status === requestedStatus) { + resolve(); + this._promisePerState[requestedStatus] = null; + } else { + const statusListener = () => { + if (this.status === requestedStatus) { + this.off('change:status', statusListener, this); + resolve(); + this._promisePerState[requestedStatus] = null; + } + }; + this.on('change:status', statusListener, this); + } + }); + } + + return this._promisePerState[requestedStatus]; + } + + queue(): void { + this.status = PlayQueueItemStatus.Queued; + } + + unQueue(): void { + this.status = PlayQueueItemStatus.Scheduled; + if (this.collection) { + const collection = this.collection; + collection.remove(this, {silent: true}); + collection.add(this, {silent: true}); + } + } + + play(startTime?: number): Promise { + if (isNumber(startTime)) { + this.progress = startTime; + } + this.status = PlayQueueItemStatus.RequestedPlaying; + return this.resolveOnStatus(PlayQueueItemStatus.Playing); + } + + pause(): Promise { + this.status = PlayQueueItemStatus.RequestedPause; + return this.resolveOnStatus(PlayQueueItemStatus.Paused); + } + + stop(): Promise { + this.status = PlayQueueItemStatus.RequestedStop; + this.progress = 0; + return this.resolveOnStatus(PlayQueueItemStatus.Stopped); + } + + seekTo(to: number): Promise { + if (this.isPlaying()) { + return this.pause().then(() => { + return this.play(to); + }); + } else { + return this.play(to); + } + } + + restart(): Promise { + return this.seekTo(0); + } + + isQueued(): boolean { + return this.status === PlayQueueItemStatus.Queued; + } + + isPlaying(): boolean { + return this.status === PlayQueueItemStatus.Playing || this.status === PlayQueueItemStatus.RequestedPlaying; + } + + isPaused(): boolean { + return this.status === PlayQueueItemStatus.Paused || this.status === PlayQueueItemStatus.RequestedPause; + } + + isStopped(): boolean { + return this.status === PlayQueueItemStatus.Stopped || this.status === PlayQueueItemStatus.RequestedStop; + } + + isScheduled(): boolean { + return this.status === PlayQueueItemStatus.Scheduled; + } + + toMiniJSON() { + const item = this.toJSON(); + item.track = this.track.toMiniJSON(); + return item; + } + + compose() { + return { + track_provider_id: this.track.provider, + track_id: this.track.id.toString(), + state: 'stopped' + }; + } + +} diff --git a/src/app/modules/api/playqueue/playqueue-item/playqueue-items-auxapp.collection.ts b/src/app/modules/api/playqueue/playqueue-item/playqueue-items-auxapp.collection.ts new file mode 100644 index 00000000..9a8a49f8 --- /dev/null +++ b/src/app/modules/api/playqueue/playqueue-item/playqueue-items-auxapp.collection.ts @@ -0,0 +1,297 @@ +import {PlaylistItemAuxappModel} from '../../playlists/playlist-item/playlist-item-auxapp.model'; +import {IPlaylistItems} from '../../playlists/playlist-item/playlist-items.interface'; +import {IPlaylistItem} from '../../playlists/playlist-item/playlist-item.interface'; +import {PlaylistItemsAuxappCollection} from '../../playlists/playlist-item/playlist-items-auxapp.collection'; +import {PlayqueueItemAuxappModel} from './playqueue-item-auxapp.model'; +import {isArray, shuffle, sortBy} from 'underscore'; +import {PlayQueueItemStatus} from '../../../player/src/playqueue-item-status.enum'; +import {PlayQueue} from '../../../player/collections/play-queue'; + +export class PlayqueueItemsAuxappCollection + extends PlaylistItemsAuxappCollection { + + private _playIndex = 0; + private _loopPlayQueue = false; + private _isShuffled = false; + + endpoint = null; + model: any = PlayqueueItemAuxappModel; + playQueueId: number; + + setEndpoint(playqueueId: number) { + this.endpoint = `/queue/${playqueueId}/item`; + this.playQueueId = playqueueId; + } + + private prepareItem(item: any): PlayqueueItemAuxappModel { + // if (!item.id && item instanceof PlayqueueItemAuxappModel) { + // item.set('id', item.track.id); + // } else if (!item.id) { + // item.id = item.track.id; + // } + + if (!(item instanceof PlayqueueItemAuxappModel) && item.indexBeforeShuffle) { + this._isShuffled = true; + } + + return item; + } + + private setPlayIndex(): number { + const currentPlaylingItem = this.getPlayingItem(); + const oldPlayIndex = this._playIndex; + if (currentPlaylingItem) { + this._playIndex = this.indexOf(currentPlaylingItem); + } + if (this._playIndex !== oldPlayIndex) { + this.trigger('change:playIndex'); + } + return this._playIndex; + } + + getQueuedItems(): TModel[] { + return this.where({status: PlayQueueItemStatus.Queued}); + } + + getScheduledItems(): TModel[] { + return this.filter((item: TModel) => { + return item.isScheduled(); + }); + } + + getStoppedItems(): TModel[] { + return this.filter((item: TModel) => { + return item.isStopped(); + }); + } + + getPlayingItem(): TModel { + return this.find(item => item.isPlaying()); + } + + getPausedItem(): TModel { + return this.find(item => item.isPaused()); + } + + getCurrentItem(): TModel { + return this.at(this._playIndex); + } + + getItem(): TModel { + const pausedItem = this.getPausedItem(); + if (pausedItem) { + return pausedItem; + } + const queuedItems = this.getQueuedItems(); + if (queuedItems.length > 0) { + return queuedItems[0]; + } else { + return this.find((playQueueItem: TModel) => { + return playQueueItem.isScheduled(); + }); + } + } + + hasNextItem(): boolean { + if (this.length === 0) { + return false; + } else if (this._loopPlayQueue) { + return true; + } else { + return this._playIndex < this.length - 1; + } + } + + hasPreviousItem(): boolean { + return this._playIndex > 0; + } + + hasCurrentItem(): boolean { + return !!this.getCurrentItem(); + } + + getRequestedPlayingItem() { + return this.find((playQueueItem) => { + return playQueueItem.status === PlayQueueItemStatus.RequestedPlaying; + }); + } + + getNextItem(): PlayqueueItemAuxappModel { + if (this.hasNextItem()) { + const total = this.length; + const next = (total + this._playIndex + 1) % total; + return this.at(next); + } + } + + getPreviousItem(): PlayqueueItemAuxappModel { + if (this.hasPreviousItem()) { + return this.at(this._playIndex - 1); + } + } + + addAndPlay(item: TModel | any): TModel { + const addItem = this.add(item, {merge: true}); + addItem.play(); + return addItem; + } + + queue(item: TModel | any): TModel { + if (!(item instanceof PlayqueueItemAuxappModel)) { + item = new PlayqueueItemAuxappModel(item); + } + if (this.get(item)) { + this.remove(item, {silent: true}); + } + item.queue(); + return this.add(item, {merge: true}); + } + + getPlayIndex(): number { + return this._playIndex; + } + + shuffle(): any { + let items = []; + let orgIndex = 0; + const currentItem = this.getCurrentItem(); + const scheduledItems = this.getScheduledItems(); + if (currentItem) { + if (!currentItem.indexBeforeShuffle) { + currentItem.indexBeforeShuffle = orgIndex++; + } + } + scheduledItems.forEach((item) => { + if (!item.indexBeforeShuffle) { + item.indexBeforeShuffle = orgIndex; + } + orgIndex++; + items.push(item); + this.remove(item, {silent: true}); + }); + items = shuffle(items); + this.add(items, {silent: true}); + this._isShuffled = true; + this.trigger('change:shuffle', this._isShuffled, this); + } + + deShuffle() { + const items = this.getScheduledItems(); + items.push(this.getCurrentItem()); + const sorted = sortBy(items, 'indexBeforeShuffle'); + sorted.forEach((item) => { + this.remove(item, {silent: true}); + this.add(item, {at: item.indexBeforeShuffle, silent: true}); + }); + this.setPlayIndex(); + this.ensureQueuingOrder(); + this.stopScheduledItemsBeforeCurrentItem(); + this._isShuffled = false; + this.trigger('change:shuffle', this._isShuffled, this); + } + + isShuffled(): boolean { + return this._isShuffled; + } + + ensureQueuingOrder(): void { + const queuedItems = this.getQueuedItems(); + const incr = this.getCurrentItem() ? 1 : 0; + queuedItems.forEach((item: TModel, index: number) => { + this.remove(item, {silent: true}); + this.setPlayIndex(); + this.add(item, {at: this.getPlayIndex() + index + incr, silent: true, doNothing: true}); + }); + } + + stopScheduledItemsBeforeCurrentItem(): void { + const scheduledItem = this.find((item: TModel) => { + return item.isScheduled(); + }); + if (scheduledItem && this.indexOf(scheduledItem) < this._playIndex) { + scheduledItem.stop(); + this.stopScheduledItemsBeforeCurrentItem(); + } + } + + scheduleStoppedItemsAfterCurrentItem(scheduledItem: TModel): void { + if (scheduledItem && scheduledItem.isStopped()) { + const index = this.indexOf(scheduledItem); + if (index > this._playIndex) { + scheduledItem.set('status', PlayQueueItemStatus.Scheduled); + this.scheduleStoppedItemsAfterCurrentItem(this.at(index - 1)); + } + } + } + + setLoopPlayQueue(allowedToLoop: boolean) { + this._loopPlayQueue = allowedToLoop; + this.trigger('change:loop', this._loopPlayQueue, this); + } + + public isLooped() { + return this._loopPlayQueue; + } + + resetQueue() { + this.filter((model) => { + return !model.isQueued(); + }).forEach((model) => { + this.remove(model); + }); + this._isShuffled = false; + } + + add(item: TModel | TModel[] | {}, options: any = {}): any { + if (options.doNothing || !item) { + return super.add(item, options); + } + + if (isArray(item)) { + const addedItems: Array = []; + item.forEach((obj: any) => { + addedItems.push(this.prepareItem(obj)); + }); + item = addedItems; + } else { + item = this.prepareItem(item); + } + + item = super.add(item, options); + + this.ensureQueuingOrder(); + this.setPlayIndex(); + + return item; + } + + initialize(): void { + this.on('change:status', (queueItem: TModel) => { + this.setPlayIndex(); + if (queueItem.isPlaying()) { + this.filter((item: TModel) => { + return item.isPlaying() || item.isPaused(); + }).forEach((playingQueueItem: PlayqueueItemAuxappModel) => { + if (playingQueueItem.id !== queueItem.id) { + playingQueueItem.stop(); + } + }); + + if (this.hasPreviousItem() && this.getPreviousItem().isScheduled()) { + this.stopScheduledItemsBeforeCurrentItem(); + } + + this.ensureQueuingOrder(); + } + + if (queueItem.isStopped()) { + queueItem.progress = 0; + this.scheduleStoppedItemsAfterCurrentItem(queueItem); + } + }); + + this.on('remove', () => { + this.setPlayIndex(); + }); + } +}