diff --git a/app/actions/index.js b/app/actions/index.js index 78365844e8..18c5dd4f1a 100644 --- a/app/actions/index.js +++ b/app/actions/index.js @@ -6,6 +6,7 @@ import globals from '../globals'; const mb = require('../rest/Musicbrainz'); const discogs = require('../rest/Discogs'); const lastfmRest = require('../rest/LastFm'); +const youtube = require('../rest/Youtube'); let lastfm = new core.LastFmApi(globals.lastfmApiKey, globals.lastfmApiSecret); @@ -33,7 +34,10 @@ export const LASTFM_ARTIST_INFO_SEARCH_SUCCESS = export const LASTFM_TRACK_SEARCH_START = 'LASTFM_TRACK_SEARCH_START'; export const LASTFM_TRACK_SEARCH_SUCCESS = 'LASTFM_TRACK_SEARCH_SUCCESS'; -export function sourcesSearch(terms, plugins) { +export const YOUTUBE_PLAYLIST_SEARCH_START = 'YOUTUBE_PLAYLIST_SEARCH_START'; +export const YOUTUBE_PLAYLIST_SEARCH_SUCCESS = 'YOUTUBE_PLAYLIST_SEARCH_SUCCESS'; + +export function sourcesSearch (terms, plugins) { let searchResults = {}; for (let i = 0; i < plugins.musicSources.length; i++) { Object.assign(searchResults, plugins.musicSources[i].search(terms)); @@ -44,27 +48,27 @@ export function sourcesSearch(terms, plugins) { }; } -export function unifiedSearchStart() { +export function unifiedSearchStart () { return { type: UNIFIED_SEARCH_START, payload: true }; } -export function unifiedSearchSuccess() { +export function unifiedSearchSuccess () { return { type: UNIFIED_SEARCH_SUCCESS, payload: false }; } -export function unifiedSearchError() { +export function unifiedSearchError () { return { type: UNIFIED_SEARCH_ERROR }; } -function discogsSearch(terms, searchType, dispatchType) { +function discogsSearch (terms, searchType, dispatchType) { return dispatch => { return searchType(terms) .then(searchResults => searchResults.json()) @@ -81,22 +85,22 @@ function discogsSearch(terms, searchType, dispatchType) { }; } -export function albumSearch(terms) { +export function albumSearch (terms) { return discogsSearch(terms, discogs.searchReleases, 'ALBUM_SEARCH_SUCCESS'); } -export function artistSearch(terms) { +export function artistSearch (terms) { return discogsSearch(terms, discogs.searchArtists, 'ARTIST_SEARCH_SUCCESS'); } -export function lastFmTrackSearchStart(terms) { +export function lastFmTrackSearchStart (terms) { return { type: LASTFM_TRACK_SEARCH_START, payload: terms }; } -export function lastFmTrackSearchSuccess(terms, searchResults) { +export function lastFmTrackSearchSuccess (terms, searchResults) { return { type: LASTFM_TRACK_SEARCH_SUCCESS, payload: { @@ -106,7 +110,7 @@ export function lastFmTrackSearchSuccess(terms, searchResults) { }; } -export function lastFmTrackSearch(terms) { +export function lastFmTrackSearch (terms) { return dispatch => { dispatch(lastFmTrackSearchStart(terms)); Promise.all([lastfmRest.searchTracks(terms)]) @@ -122,16 +126,60 @@ export function lastFmTrackSearch(terms) { }; } -export function unifiedSearch(terms, history) { +export function youtubePlaylistSearchStart (terms) { + return { + type: YOUTUBE_PLAYLIST_SEARCH_START, + payload: terms + }; +} + +export function youtubePlaylistSearchSuccess (terms, results) { + return { + type: YOUTUBE_PLAYLIST_SEARCH_SUCCESS, + payload: { + id: terms, + info: results + } + }; +} + +export function youtubePlaylistSearch (terms) { + return dispatch => { + dispatch(youtubePlaylistSearchStart(terms)); + + youtube.playlistSearch(terms) + .then(results => { + //console.log(results) + dispatch( + youtubePlaylistSearchSuccess(terms, results) + ); + }) + .catch(error => { + logger.error(error); + }); + /*Promise.all([lastfmRest.searchTracks(terms)]) + .then(results => Promise.all(results.map(info => info.json()))) + .then(results => { + dispatch( + //youtubePlaylistSearchSuccess(terms, results[0].results.trackmatches.track) + ); + }) + .catch(error => { + logger.error(error); + });*/ + }; +} + +export function unifiedSearch (terms, history) { return dispatch => { dispatch(unifiedSearchStart()); Promise.all([ dispatch(albumSearch(terms)), dispatch(artistSearch(terms)), - dispatch(lastFmTrackSearch(terms)) + dispatch(lastFmTrackSearch(terms)), + dispatch(youtubePlaylistSearch(terms)) ]) - .then(() => { dispatch(unifiedSearchSuccess()); if (history.location.pathname !== '/search') { @@ -145,14 +193,14 @@ export function unifiedSearch(terms, history) { }; } -export function albumInfoStart(albumId) { +export function albumInfoStart (albumId) { return { type: ALBUM_INFO_SEARCH_START, payload: albumId }; } -export function albumInfoSuccess(albumId, info) { +export function albumInfoSuccess (albumId, info) { return { type: ALBUM_INFO_SEARCH_SUCCESS, payload: { @@ -162,7 +210,7 @@ export function albumInfoSuccess(albumId, info) { }; } -export function albumInfoSearch(albumId, releaseType) { +export function albumInfoSearch (albumId, releaseType) { return dispatch => { dispatch(albumInfoStart(albumId)); discogs @@ -183,14 +231,14 @@ export function albumInfoSearch(albumId, releaseType) { }; } -export function artistInfoStart(artistId) { +export function artistInfoStart (artistId) { return { type: ARTIST_INFO_SEARCH_START, payload: artistId }; } -export function artistInfoSuccess(artistId, info) { +export function artistInfoSuccess (artistId, info) { return { type: ARTIST_INFO_SEARCH_SUCCESS, payload: { @@ -200,7 +248,7 @@ export function artistInfoSuccess(artistId, info) { }; } -export function artistInfoSearch(artistId) { +export function artistInfoSearch (artistId) { return dispatch => { dispatch(artistInfoStart(artistId)); discogs @@ -216,14 +264,14 @@ export function artistInfoSearch(artistId) { }; } -export function artistReleasesStart(artistId) { +export function artistReleasesStart (artistId) { return { type: ARTIST_RELEASES_SEARCH_START, payload: artistId }; } -export function artistReleasesSuccess(artistId, releases) { +export function artistReleasesSuccess (artistId, releases) { return { type: ARTIST_RELEASES_SEARCH_SUCCESS, payload: { @@ -233,7 +281,7 @@ export function artistReleasesSuccess(artistId, releases) { }; } -export function artistReleasesSearch(artistId) { +export function artistReleasesSearch (artistId) { return dispatch => { dispatch(artistReleasesStart(artistId)); discogs @@ -248,7 +296,7 @@ export function artistReleasesSearch(artistId) { }; } -export function artistInfoSearchByName(artistName, history) { +export function artistInfoSearchByName (artistName, history) { return dispatch => { discogs .searchArtists(artistName) @@ -267,14 +315,14 @@ export function artistInfoSearchByName(artistName, history) { }; } -export function lastFmArtistInfoStart(artistId) { +export function lastFmArtistInfoStart (artistId) { return { type: LASTFM_ARTIST_INFO_SEARCH_START, payload: artistId }; } -export function lastFmArtistInfoSuccess(artistId, info) { +export function lastFmArtistInfoSuccess (artistId, info) { return { type: LASTFM_ARTIST_INFO_SEARCH_SUCCESS, payload: { @@ -284,7 +332,7 @@ export function lastFmArtistInfoSuccess(artistId, info) { }; } -export function lastFmArtistInfoSearch(artist, artistId) { +export function lastFmArtistInfoSearch (artist, artistId) { return dispatch => { dispatch(lastFmArtistInfoStart(artistId)); Promise.all([ @@ -305,3 +353,5 @@ export function lastFmArtistInfoSearch(artist, artistId) { }); }; } + + diff --git a/app/components/SearchResults/AllResults/index.js b/app/components/SearchResults/AllResults/index.js index ae2e624ef7..d58f9e685b 100644 --- a/app/components/SearchResults/AllResults/index.js +++ b/app/components/SearchResults/AllResults/index.js @@ -1,6 +1,9 @@ import React from 'react'; +import artPlaceholder from '../../../../resources/media/art_placeholder.png'; import Card from '../../Card'; +import PlaylistResults from '../PlaylistResults' +import TracksResults from '../TracksResults' import styles from './styles.scss'; @@ -8,43 +11,78 @@ class AllResults extends React.Component { constructor(props) { super(props); } - - renderResults(collection, onClick) { + renderResults (collection, onClick) { return collection.slice(0, 5).map((el, i) => { return ( onClick(el.id, el.type)} - key={i} + key={'item-' + i} /> ); }); } - renderLastFmResults(collection = []) { - let addToQueue = this.props.addToQueue; - return (collection || []).slice(0, 5).map((el, i) => { - return ( - { - addToQueue(this.props.musicSources, { - artist: el.name, - name: el.name, - thumbnail: el.image[1]['#text'] - }); - }} - key={i} - /> - ); - }); + renderTracks (arr = [], limit = 5) { + return () } - render() { + renderPlaylistSection () { + return ( +
+

Playlist

+
+ +
+
) + } + + renderArtistsSection () { + return (
+

Artists

+
+ {this.renderResults( + this.props.artistSearchResults, + this.props.artistInfoSearch + )} +
+
) + } + + renderAlbumsSection () { + return (
+

Albums

+
+ {this.renderResults( + this.props.albumSearchResults, + this.props.albumInfoSearch + )} +
+
) + } + + renderTracksSection () { + return (
+

Tracks

+
+ {this.renderTracks(this.props.trackSearchResults.info)} +
+
) + } + + render () { if ( this.props.artistSearchResults.length <= 0 && this.props.albumSearchResults.length <= 0 && @@ -55,32 +93,10 @@ class AllResults extends React.Component { return (
-
-

Artists

-
- {this.renderResults( - this.props.artistSearchResults, - this.props.artistInfoSearch - )} -
-
- -
-

Albums

-
- {this.renderResults( - this.props.albumSearchResults, - this.props.albumInfoSearch - )} -
-
- -
-

Tracks

-
- {this.renderLastFmResults(this.props.trackSearchResults.info)} -
-
+ {this.renderArtistsSection()} + {this.renderAlbumsSection()} + {this.renderTracksSection()} + {this.renderPlaylistSection()}
); } diff --git a/app/components/SearchResults/PlaylistResults/index.js b/app/components/SearchResults/PlaylistResults/index.js new file mode 100644 index 0000000000..110314ff14 --- /dev/null +++ b/app/components/SearchResults/PlaylistResults/index.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import TracksResults from '../TracksResults'; + +import styles from './styles.scss'; + +class PlaylistResults extends React.Component { + constructor(props) { + super(props); + } + render () { + return ( + this.props.playlistSearchStarted ? ((this.props.playlistSearchStarted.length > 0 && this.props.playlistSearchResults.info == undefined) ?
Loading...
: ()) :
No result
+ ) + } +} + +export default PlaylistResults; \ No newline at end of file diff --git a/app/components/SearchResults/PlaylistResults/styles.scss b/app/components/SearchResults/PlaylistResults/styles.scss new file mode 100644 index 0000000000..356f6f080a --- /dev/null +++ b/app/components/SearchResults/PlaylistResults/styles.scss @@ -0,0 +1,22 @@ +.all_results_container { + display: flex; + flex-flow: row wrap; + justify-content: space-around; + width: 100%; + + .column { + display: flex; + flex-flow: column; + width: 100%; + + h3 { + font-variant: small-caps; + margin: 1rem; + } + + .row { + display: flex; + flex-flow: row wrap; + } + } +} diff --git a/app/components/SearchResults/TracksResults/index.js b/app/components/SearchResults/TracksResults/index.js new file mode 100644 index 0000000000..8824fd56ca --- /dev/null +++ b/app/components/SearchResults/TracksResults/index.js @@ -0,0 +1,52 @@ +import React from 'react'; +import artPlaceholder from '../../../../resources/media/art_placeholder.png'; + +import styles from './styles.scss'; + +class TracksResults extends React.Component { + constructor(props) { + super(props); + } + + renderTrack (track, index = 0) { + let addToQueue = this.props.addToQueue; + return ( +
{ + addToQueue(this.props.musicSources, { + artist: track.name, + name: track.name, + thumbnail: track.image[1]['#text'] + }); + }}> + +
{track.name}
+ +
+ ); + } + + render () { + + let collection = this.props.tracks || [] + let limit = this.props.limit + if (collection.length == 0) { + return "No result" + } else { + return ( +
+ { + (collection || []).slice(0, limit).map((track, i) => { + if (track) { + if (track.name && track.image && track.artist) { + { return this.renderTrack(track, i) } + } + } + }) + } +
+ ); + } + } +} + +export default TracksResults; \ No newline at end of file diff --git a/app/components/SearchResults/TracksResults/styles.scss b/app/components/SearchResults/TracksResults/styles.scss new file mode 100644 index 0000000000..361a56854a --- /dev/null +++ b/app/components/SearchResults/TracksResults/styles.scss @@ -0,0 +1,92 @@ + +@import '../../../vars'; + +.all_results_container { + display: flex; + flex-flow: row wrap; + justify-content: space-around; + width: 100%; + + .column { + display: flex; + flex-flow: column; + width: 100%; + + h3 { + font-variant: small-caps; + margin: 1rem; + } + + .row { + display: flex; + flex-flow: row wrap; + } + } +} + + +.popular_tracks_container { + display: flex; + flex: 1 1 auto; + flex-flow: column; + + margin: 0 0.5rem; + + .header { + margin-bottom: 1rem; + + font-size: 16px; + font-variant: small-caps; + } + + .track_row { + display: flex; + align-items: center; + flex-flow: row; + + transition: $short-duration ease-in-out; + + border-bottom: 1px solid rgba($background2, 0.2); + + &:hover { + background: lighten($background, 10%); + } + + img { + flex: 0 0 auto; + + width: 3rem; + height: 3rem; + } + + .popular_track_name { + flex: 1 1 auto; + margin: 0.25rem 0.5rem; + text-align: left; + } + + .playcount { + flex: 1 1 auto; + margin: 0.25rem 0.5rem; + text-align: right; + text-transform: uppercase; + } + } + + .expand_button { + display: flex; + justify-content: center; + padding: 0.5rem; + margin-top: 0.5rem; + transition: $short-duration; + cursor: pointer; + + &:hover { + background: lighten($background, 5%); + } + } +} + +.add_button { + text-align: left; +} diff --git a/app/components/SearchResults/index.js b/app/components/SearchResults/index.js index 214edcc885..36ee304dc4 100644 --- a/app/components/SearchResults/index.js +++ b/app/components/SearchResults/index.js @@ -2,30 +2,36 @@ import React from 'react'; import { Tab } from 'semantic-ui-react'; import AllResults from './AllResults'; +import TracksResults from './TracksResults'; +import PlaylistResults from './PlaylistResults'; import Card from '../Card'; import styles from './styles.scss'; class SearchResults extends React.Component { - renderAllResultsPane() { + renderAllResultsPane () { return (
- +
+ +
-
+ ); } - renderPane(collection, onClick) { + renderPane (collection, onClick) { return (
@@ -55,32 +61,19 @@ class SearchResults extends React.Component { ); } - renderLastFmPane(collection) { + renderLastFmPane (collection) { if (collection !== undefined) { - let addToQueue = this.props.addToQueue; return (
{collection.length > 0 ? this.props.unifiedSearchStarted ? null - : collection.map((el, i) => { - return ( - { - addToQueue(this.props.musicSources, { - artist: el.name, - name: el.name, - thumbnail: el.image[1]['#text'] - }); - }} - key={'lastfm-card-' + i} - /> - ); - }) + : : 'Nothing found.'}
@@ -94,7 +87,16 @@ class SearchResults extends React.Component { } } - panes() { + renderPlaylistPane () { + return () + } + + panes () { let panes = [ { menuItem: 'All', @@ -120,22 +122,27 @@ class SearchResults extends React.Component { menuItem: 'Tracks', render: () => this.renderLastFmPane(this.props.trackSearchResults.info) } + , + { + menuItem: 'Playlist', + render: () => this.renderPlaylistPane(this.props.playlistSearchResults) + } ]; return panes; } - albumInfoSearch(albumId, releaseType) { + albumInfoSearch (albumId, releaseType) { this.props.albumInfoSearch(albumId, releaseType); this.props.history.push('/album/' + albumId); } - artistInfoSearch(artistId) { + artistInfoSearch (artistId) { this.props.artistInfoSearch(artistId); this.props.history.push('/artist/' + artistId); } - render() { + render () { return (
diff --git a/app/containers/SearchResultsContainer/index.js b/app/containers/SearchResultsContainer/index.js index e4326afd01..cacac65613 100644 --- a/app/containers/SearchResultsContainer/index.js +++ b/app/containers/SearchResultsContainer/index.js @@ -10,7 +10,7 @@ class SearchResultsContainer extends React.Component { constructor(props) { super(props); } - render() { + render () { let { actions, musicSources } = this.props; return ( @@ -18,7 +18,9 @@ class SearchResultsContainer extends React.Component { artistSearchResults={this.props.artistSearchResults} albumSearchResults={this.props.albumSearchResults} trackSearchResults={this.props.trackSearchResults} + playlistSearchResults={this.props.playlistSearchResults} unifiedSearchStarted={this.props.unifiedSearchStarted} + playlistSearchStarted={this.props.playlistSearchStarted} albumInfoSearch={this.props.actions.albumInfoSearch} artistInfoSearch={this.props.actions.artistInfoSearch} history={this.props.history} @@ -29,17 +31,19 @@ class SearchResultsContainer extends React.Component { } } -function mapStateToProps(state) { +function mapStateToProps (state) { return { artistSearchResults: state.search.artistSearchResults, albumSearchResults: state.search.albumSearchResults, trackSearchResults: state.search.trackSearchResults, + playlistSearchResults: state.search.playlistSearchResults, unifiedSearchStarted: state.search.unifiedSearchStarted, + playlistSearchStarted: state.search.playlistSearchStarted, musicSources: state.plugin.plugins.musicSources }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps (dispatch) { return { actions: bindActionCreators( Object.assign({}, Actions, QueueActions), diff --git a/app/reducers/search.js b/app/reducers/search.js index cac1bad98c..1dfab75a3c 100644 --- a/app/reducers/search.js +++ b/app/reducers/search.js @@ -13,7 +13,9 @@ import { LASTFM_ARTIST_INFO_SEARCH_START, LASTFM_ARTIST_INFO_SEARCH_SUCCESS, LASTFM_TRACK_SEARCH_START, - LASTFM_TRACK_SEARCH_SUCCESS + LASTFM_TRACK_SEARCH_SUCCESS, + YOUTUBE_PLAYLIST_SEARCH_START, + YOUTUBE_PLAYLIST_SEARCH_SUCCESS } from '../actions'; let _ = require('lodash'); @@ -23,35 +25,37 @@ const initialState = { artistSearchResults: [], albumSearchResults: [], trackSearchResults: [], + playlistSearchResults: [], albumDetails: {}, artistDetails: {}, - unifiedSearchStarted: false + unifiedSearchStarted: false, + playlistSearchStarted: false }; -function reduceUnifiedSearchStart(state, action) { +function reduceUnifiedSearchStart (state, action) { return Object.assign({}, state, { unifiedSearchStarted: action.payload }); } -function reduceAlbumSearchSuccess(state, action) { +function reduceAlbumSearchSuccess (state, action) { return Object.assign({}, state, { albumSearchResults: action.payload }); } -function reduceArtistSearchSuccess(state, action) { +function reduceArtistSearchSuccess (state, action) { return Object.assign({}, state, { artistSearchResults: action.payload }); } -function reduceUnifiedSearchSuccess(state, action) { +function reduceUnifiedSearchSuccess (state, action) { return Object.assign({}, state, { unifiedSearchStarted: action.payload }); } -function reduceAlbumInfoSearchStart(state, action) { +function reduceAlbumInfoSearchStart (state, action) { return Object.assign({}, state, { albumDetails: Object.assign({}, state.albumDetails, { [`${action.payload}`]: Object.assign({}, { loading: true }) @@ -59,7 +63,7 @@ function reduceAlbumInfoSearchStart(state, action) { }); } -function reduceAlbumInfoSearchSuccess(state, action) { +function reduceAlbumInfoSearchSuccess (state, action) { return Object.assign({}, state, { albumDetails: Object.assign({}, state.albumDetails, { [`${action.payload.id}`]: Object.assign({}, action.payload.info, { @@ -69,7 +73,7 @@ function reduceAlbumInfoSearchSuccess(state, action) { }); } -function reduceArtistInfoSearchStart(state, action) { +function reduceArtistInfoSearchStart (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload}`]: { @@ -79,7 +83,7 @@ function reduceArtistInfoSearchStart(state, action) { }); } -function reduceArtistInfoSearchSuccess(state, action) { +function reduceArtistInfoSearchSuccess (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload.id}`]: Object.assign({}, action.payload.info, { @@ -88,7 +92,7 @@ function reduceArtistInfoSearchSuccess(state, action) { }) }); } -function reduceArtistReleasesSearchStart(state, action) { +function reduceArtistReleasesSearchStart (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload}`]: Object.assign( @@ -100,7 +104,7 @@ function reduceArtistReleasesSearchStart(state, action) { }); } -function reduceArtistReleasesSearchSuccess(state, action) { +function reduceArtistReleasesSearchSuccess (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload.id}`]: Object.assign( @@ -112,7 +116,7 @@ function reduceArtistReleasesSearchSuccess(state, action) { }); } -function reduceLastfmArtistInfoSearchStart(state, action) { +function reduceLastfmArtistInfoSearchStart (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload}`]: Object.assign( @@ -124,7 +128,7 @@ function reduceLastfmArtistInfoSearchStart(state, action) { }); } -function reduceLastfmArtistInfoSearchSuccess(state, action) { +function reduceLastfmArtistInfoSearchSuccess (state, action) { return Object.assign({}, state, { artistDetails: Object.assign({}, state.artistDetails, { [`${action.payload.id}`]: Object.assign( @@ -140,49 +144,66 @@ function reduceLastfmArtistInfoSearchSuccess(state, action) { }); } -function reduceLastfmTrackSearchStart(state, action) { +function reduceLastfmTrackSearchStart (state, action) { return Object.assign({}, state, { - trackSearchStarted: action.payload + trackSearchResults: action.payload }); } -function reduceLastfmTrackSearchSuccess(state, action) { +function reduceLastfmTrackSearchSuccess (state, action) { return Object.assign({}, state, { trackSearchResults: action.payload }); } -export default function SearchReducer(state = initialState, action) { +function reduceYoutubePlaylistSearchStart (state, action) { + return Object.assign({}, state, { + playlistSearchStarted: action.payload, + playlistSearchResults: [] + }); +} + +function reduceYoutubePlaylistSearchSuccess (state, action) { + return Object.assign({}, state, { + playlistSearchResults: action.payload + }); +} + +export default function SearchReducer (state = initialState, action) { switch (action.type) { - case UNIFIED_SEARCH_START: - return reduceUnifiedSearchStart(state, action); - case ALBUM_SEARCH_SUCCESS: - return reduceAlbumSearchSuccess(state, action); - case ARTIST_SEARCH_SUCCESS: - return reduceArtistSearchSuccess(state, action); - case UNIFIED_SEARCH_SUCCESS: - return reduceUnifiedSearchSuccess(state, action); - case ALBUM_INFO_SEARCH_START: - return reduceAlbumInfoSearchStart(state, action); - case ALBUM_INFO_SEARCH_SUCCESS: - return reduceAlbumInfoSearchSuccess(state, action); - case ARTIST_INFO_SEARCH_START: - return reduceArtistInfoSearchStart(state, action); - case ARTIST_INFO_SEARCH_SUCCESS: - return reduceArtistInfoSearchSuccess(state, action); - case ARTIST_RELEASES_SEARCH_START: - return reduceArtistReleasesSearchStart(state, action); - case ARTIST_RELEASES_SEARCH_SUCCESS: - return reduceArtistReleasesSearchSuccess(state, action); - case LASTFM_ARTIST_INFO_SEARCH_START: - return reduceLastfmArtistInfoSearchStart(state, action); - case LASTFM_ARTIST_INFO_SEARCH_SUCCESS: - return reduceLastfmArtistInfoSearchSuccess(state, action); - case LASTFM_TRACK_SEARCH_START: - return reduceLastfmTrackSearchStart(state, action); - case LASTFM_TRACK_SEARCH_SUCCESS: - return reduceLastfmTrackSearchSuccess(state, action); - default: - return state; + case UNIFIED_SEARCH_START: + return reduceUnifiedSearchStart(state, action); + case ALBUM_SEARCH_SUCCESS: + return reduceAlbumSearchSuccess(state, action); + case ARTIST_SEARCH_SUCCESS: + return reduceArtistSearchSuccess(state, action); + case UNIFIED_SEARCH_SUCCESS: + return reduceUnifiedSearchSuccess(state, action); + case ALBUM_INFO_SEARCH_START: + return reduceAlbumInfoSearchStart(state, action); + case ALBUM_INFO_SEARCH_SUCCESS: + return reduceAlbumInfoSearchSuccess(state, action); + case ARTIST_INFO_SEARCH_START: + return reduceArtistInfoSearchStart(state, action); + case ARTIST_INFO_SEARCH_SUCCESS: + return reduceArtistInfoSearchSuccess(state, action); + case ARTIST_RELEASES_SEARCH_START: + return reduceArtistReleasesSearchStart(state, action); + case ARTIST_RELEASES_SEARCH_SUCCESS: + return reduceArtistReleasesSearchSuccess(state, action); + case LASTFM_ARTIST_INFO_SEARCH_START: + return reduceLastfmArtistInfoSearchStart(state, action); + case LASTFM_ARTIST_INFO_SEARCH_SUCCESS: + return reduceLastfmArtistInfoSearchSuccess(state, action); + case LASTFM_TRACK_SEARCH_START: + return reduceLastfmTrackSearchStart(state, action); + case LASTFM_TRACK_SEARCH_SUCCESS: + return reduceLastfmTrackSearchSuccess(state, action); + case YOUTUBE_PLAYLIST_SEARCH_START: + return reduceYoutubePlaylistSearchStart(state, action); + case YOUTUBE_PLAYLIST_SEARCH_SUCCESS: + return reduceYoutubePlaylistSearchSuccess(state, action); + default: + return state; } } diff --git a/app/rest/LastFm.js b/app/rest/LastFm.js index 9dccad0d07..78230fb6cc 100644 --- a/app/rest/LastFm.js +++ b/app/rest/LastFm.js @@ -5,18 +5,19 @@ import globals from '../globals'; let lastfmApiKey = globals.lastfmApiKey; let lastfmApiSecret = globals.lastfmApiSecret; -function makeLastfmRequest(parameters) { +function makeLastfmRequest (parameters) { return fetch( apiUrl + parameters + '&api_key=' + lastfmApiKey + '&format=json' ); } -function searchTracks(terms) { +function searchTracks (terms, limit = 30) { let parameters = 'track.search&track=' + encodeURI(terms); + parameters += '&limit=' + limit return makeLastfmRequest(parameters); } -function getTopTracks() { +function getTopTracks () { let parameters = 'chart.gettoptracks'; return makeLastfmRequest(parameters); } diff --git a/app/rest/Youtube.js b/app/rest/Youtube.js index 09efa08ece..7c6ec2e90a 100644 --- a/app/rest/Youtube.js +++ b/app/rest/Youtube.js @@ -1,9 +1,53 @@ import globals from '../globals'; +import { lastFmTrackSearchStart } from '../actions'; +const ytlist = require('youtube-playlist'); +const getArtistTitle = require('get-artist-title') +const lastfm = require('./LastFm') -function prepareUrl(url) { +function prepareUrl (url) { return `${url}&key=${globals.ytApiKey}`; } -export function trackSearch(track) { - return fetch(prepareUrl('https://www.googleapis.com/youtube/v3/search?part=id,snippet&type=video&maxResults=50&q='+encodeURIComponent(track))); +export function trackSearch (track) { + return fetch(prepareUrl('https://www.googleapis.com/youtube/v3/search?part=id,snippet&type=video&maxResults=50&q=' + encodeURIComponent(track))); } + +export function playlistSearch (url) { + if (isValidURL(url)) { + console.log("Searching playlist : " + url) + return ytlist(url, 'name') + .then(res => { + let allTracks = res.data.playlist.map((elt, i) => { + let result = getArtistTitle(elt) + if (result !== undefined) { + return lastfm.searchTracks(result[0] + ' ' + result[1], 1) + .then(tracks => tracks.json()) + .then(tracksJson => { + return new Promise((resolve, reject) => { + resolve(tracksJson.results.trackmatches.track[0]) + }) + }) + } else { + return new Promise((resolve, reject) => { resolve({}) }) + } + }) + return Promise.all(allTracks) + }) + .catch(function (err) { + return new Promise((resolve, reject) => { resolve([]) }) + }); + } else { + return new Promise((resolve, reject) => { resolve([]) }) + } +} + +function isValidURL (str) { + var pattern = new RegExp('^(https?:\\/\\/)' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name and extension + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?' + // port + '(\\/[-a-z\\d%@_.~+&:]*)*' + // path + '(\\?[;&a-z\\d%@_.,~+&:=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return pattern.test(str); +} \ No newline at end of file diff --git a/package.json b/package.json index 5714228be2..fe1b7ad162 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "electron-store": "^2.0.0", "electron-timber": "^0.5.1", "font-awesome": "^4.7.0", + "get-artist-title": "^1.1.1", "md5": "^2.2.1", "moment": "^2.20.1", "nuclear-core": "0.0.4", @@ -53,7 +54,7 @@ "numeral": "^2.0.6", "pitchfork-bnm": "^1.0.3", "react": "^16.3.2", - "react-beautiful-dnd": "^10.0.3", + "react-beautiful-dnd": "^9.0.0", "react-dom": "^16.3.2", "react-image-smooth-loading": "^2.0.0", "react-router-transition": "^1.2.1", @@ -62,6 +63,7 @@ "styled-components": "^3.2.6", "uuid": "^3.2.1", "webpack-cli": "^3.0.8", + "youtube-playlist": "^1.0.2", "ytdl-core": "^0.24.0" }, "devDependencies": {