Skip to content

Commit

Permalink
Save track id from stream provider (#1048)
Browse files Browse the repository at this point in the history
* save track id from stream provider when favoriting

* add youtube provider get track by id

* add getStreamForId for bandcamp + audius

* update other stream provider

* add unit test

* playqueue test for favorite

* add favorite track container test
  • Loading branch information
haidang666 authored Oct 5, 2021
1 parent 5891b8b commit 00c7260
Show file tree
Hide file tree
Showing 30 changed files with 974 additions and 57 deletions.
4 changes: 2 additions & 2 deletions packages/app/app/actions/favorites.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'lodash';
import { store } from '@nuclear/core';
import { areTracksEqualByName, getTrackItem } from '@nuclear/ui';
import { areTracksEqualByName, getTrackItem, removeTrackStreamUrl } from '@nuclear/ui';

import { safeAddUuid } from './helpers';

Expand All @@ -23,7 +23,7 @@ export function readFavorites() {
}

export function addFavoriteTrack(track) {
const clonedTrack = safeAddUuid(getTrackItem(track));
const clonedTrack = safeAddUuid(getTrackItem(removeTrackStreamUrl(track)));

const favorites = store.get('favorites');
const filteredTracks = favorites.tracks.filter(t => !areTracksEqualByName(t, track));
Expand Down
41 changes: 37 additions & 4 deletions packages/app/app/actions/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import _ from 'lodash';

import { safeAddUuid } from './helpers';
import { startPlayback } from './player.js';
import { Track } from '@nuclear/ui/lib/types';
import { StreamProvider } from '@nuclear/core';

export const QUEUE_DROP = 'QUEUE_DROP';
export const ADD_QUEUE_ITEM = 'ADD_QUEUE_ITEM';
Expand All @@ -27,6 +29,31 @@ const getSelectedStreamProvider = getState => {
return _.find(streamProviders, { sourceName: selected.streamProviders });
};

const getTrackStream = async (track: Track, selectedStreamProvider: StreamProvider, streamProviders: StreamProvider[]) => {
let streamData;
if (track?.streams && track.streams.length) {
const matchSelectedProvider = track.streams.find(
s => s.source === selectedStreamProvider.sourceName);

if (matchSelectedProvider && matchSelectedProvider.id) {
streamData = await selectedStreamProvider.getStreamForId(matchSelectedProvider.id);
} else {
const firstKnownProvider = streamProviders.find(
s => s.sourceName === track.streams[0].source);
streamData = await firstKnownProvider.getStreamForId(track.streams[0].id);
}
}

if (!streamData) {
streamData = await selectedStreamProvider.search({
artist: typeof track.artist === 'string' ? track.artist : track.artist.name,
track: track.name
});
}

return streamData;
};

const addQueueItem = item => ({
type: ADD_QUEUE_ITEM,
payload: { item }
Expand All @@ -46,18 +73,24 @@ export const addToQueue = (item, asNextItem = false) => async (dispatch, getStat
item.loading = !item.local;
item = safeAddUuid(item);

const { connectivity } = getState();
const {
connectivity,
plugin: {
plugins: { streamProviders }
}
} = getState();
const isAbleToAdd = (!connectivity && item.local) || connectivity;

isAbleToAdd && dispatch(!asNextItem ? addQueueItem(item) : playNextItem(item));

if (!item.local && isAbleToAdd) {
const selectedStreamProvider = getSelectedStreamProvider(getState);
try {
const streamData = await selectedStreamProvider.search({
const streamData = await getTrackStream({
artist: item.artist,
track: item.name
});
name: item.name,
streams: item.streams
}, selectedStreamProvider, streamProviders);

if (streamData === undefined){
dispatch(removeFromQueue(item));
Expand Down
1 change: 1 addition & 0 deletions packages/app/app/components/FavoriteTracksView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const FavoriteTracksView = ({
key={'popular-track-row-' + index}
trigger={
<TrackRow
data-testid={`fav-track-${track.uuid}`}
key={'favorite-track-' + index}
index={index}
track={track}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const QueueMenuMore = ({
const { t } = useTranslation('queue');

return (
<Dropdown item icon='ellipsis vertical' className={styles.queue_menu_more} disabled={disabled}>
<Dropdown item icon='ellipsis vertical' data-testid='queue-more-container' className={styles.queue_menu_more} disabled={disabled}>
<Dropdown.Menu>
<Dropdown.Header>{t('header')}</Dropdown.Header>
<Dropdown.Item onClick={handleClearClick}>
Expand Down Expand Up @@ -64,6 +64,7 @@ export const QueueMenuMore = ({
</Dropdown.Item>
<Dropdown.Item
onClick={handleAddFavoriteTrack}
data-testid='queue-more-favorite'
>
<Icon name='star' />
{t('favorite-add')}
Expand Down Expand Up @@ -115,7 +116,8 @@ export const enhance = compose(
{
'#text': currentItem.thumbnail
}
]
],
streams: currentItem.streams
});
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@

import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import _ from 'lodash';

import { buildStoreState } from '../../../test/storeBuilders';
import { AnyProps, configureMockStore, setupI18Next, TestRouterProvider, TestStoreProvider } from '../../../test/testUtils';
import MainContentContainer from '../MainContentContainer';

const updateStore = (key: string, value: object) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const store = require('@nuclear/core').store;
store.set(key, value);
};

describe('Track view container', () => {
beforeAll(() => {
setupI18Next();
});

beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { store } = require('@nuclear/core');
store.clear();
});

it('should display favorite tracks', () => {
const favorites = buildStoreState()
.withFavorites()
.build()
.favorites;
updateStore('favorites', favorites);
const { component } = mountComponent();
expect(component.asFragment()).toMatchSnapshot();
});

it('should display empty state', () => {
const { component } = mountComponent();
expect(component.asFragment()).toMatchSnapshot();
});

it('should show popup when click on track', async () => {
const favorites = buildStoreState()
.withFavorites()
.build()
.favorites;
updateStore('favorites', favorites);
const { component } = mountComponent();

await waitFor(() => component.getByTestId('fav-track-uuid1').click());
expect(component.asFragment()).toMatchSnapshot();
});

it('should call provider.search when play track with no stream', async () => {
const favorites = buildStoreState()
.withFavorites()
.build()
.favorites;
updateStore('favorites', favorites);

const { component, store } = mountComponent();
const state = store.getState();
const selectedStreamProvider = _.find(
state.plugin.plugins.streamProviders,
{ sourceName: state.plugin.selected.streamProviders });

await waitFor(() => component.getByTestId('fav-track-uuid2').click());
await waitFor(() => component.getByTestId('track-popup-add-queue').click());

expect(selectedStreamProvider.search).toBeCalled();
expect(selectedStreamProvider.getStreamForId).not.toBeCalled();
});

it('should call provider.getStreamForId when play track with known stream', async () => {
const favorites = buildStoreState()
.withFavorites()
.build()
.favorites;
updateStore('favorites', favorites);

const { component, store } = mountComponent();
const state = store.getState();
const selectedStreamProvider = _.find(
state.plugin.plugins.streamProviders,
{ sourceName: state.plugin.selected.streamProviders });

await waitFor(() => component.getByTestId('fav-track-uuid1').click());
await waitFor(() => component.getByTestId('track-popup-add-queue').click());

expect(selectedStreamProvider.search).not.toBeCalled();
expect(selectedStreamProvider.getStreamForId).toBeCalled();
});

const mountComponent = (initialStore?: AnyProps) => {
const initialState = initialStore ||
buildStoreState()
.withPlugins()
.withConnectivity()
.build();

const history = createMemoryHistory({
initialEntries: ['/favorites/tracks']
});
const store = configureMockStore(initialState);
const component = render(
<TestRouterProvider
history={history}
>
<TestStoreProvider
store={store}
>
<MainContentContainer />
</TestStoreProvider>
</TestRouterProvider >, {container: document.body}
);
return { component, history, store };
};
});
Loading

0 comments on commit 00c7260

Please sign in to comment.