Skip to content

Commit

Permalink
Rewrite with web components and batch import to R4
Browse files Browse the repository at this point in the history
  • Loading branch information
oskarrough committed May 13, 2023
1 parent afff2db commit 74a786f
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 159 deletions.
28 changes: 2 additions & 26 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,11 @@
<body>

<header>
<h1>Spotify &rarr; YouTube</h1>
<p><big>Convert a Spotify playlist to a list of YouTube videos</big></p>
<h1>Convert Spotify playlist &rarr; YouTube videos &rarr; Radio4000 tracks</h1>
</header>

<main>
<p>
First, <a href="https://developer.spotify.com/dashboard/">find your Spotify API <code>client id</code> and
<code>secret</code>.</a><br>
If you already have a token, this is not needed.
</p>


<form id="spotifyToYoutube">
<label for="clientId">Client id</label>
<input type="text" name="clientId" /><br>
<label for="clientSecret">Client secret</label>
<input type="text" name="clientSecret" /><br>
<label for="token">Spotify token</label>
<input name="token"><br>
<label for="url">Playlist URL</label>
<input
type="text"
name="url"
value="https://open.spotify.com/playlist/7kqQXkLFuIZFScIuXFaJHe?si=a07c2e4802c54886"
required
/><br>
<button type="submit">Find matches</button>
</form>
<div id="app"></div>
<spotify-to-youtube></spotify-to-youtube>
</main>

<footer>
Expand Down
70 changes: 38 additions & 32 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,68 +14,74 @@ body {

body > header,
body > main {
margin: 0 1rem;
margin: 0 1rem;
}

body > footer {
margin: auto 1rem 0 auto;
margin: auto 1rem 0 auto;
}

ul ul li {
display: flex;
display: flex;
}

textarea {
min-height: 5em;
width: 60ch;
max-width: 100%;
min-height: 5em;
width: 60ch;
max-width: 100%;
}

.tracks {}
.results {
margin-top: 1rem;
padding-left: 0rem;
list-style: none;
.tracks {
display: flex;
flex-direction: column;
padding: 0;
list-style: none;
gap: 1em;
}

.results label {
/* padding-top: 1em; */
.results {
display: flex;
flex-direction: row;
padding-left: 0rem;
list-style: none;
}

.results label {}
.results > li {
margin: 1em 0;
margin: 1em 0;
}
.results ul {
padding-left: 1em;
padding-left: 1em;
}

.results:has(input[type="radio"]:checked) li ul {
opacity: 0.25;
.results:has(input[type='radio']:checked) li ul {
opacity: 0.25;
}

.results li:has(input[type="radio"]:checked) ul {
/* outline: 1px solid green; */
opacity: 1;
.results li:has(input[type='radio']:checked) ul {
/* outline: 1px solid green; */
opacity: 1;
}

.results img {
height: 3rem;
vertical-align: top;
width: 5rem;
/* display: none; */
vertical-align: top;
}

a {
color: blue;
color: blue;
}

label {
display: inline-block;
max-width: 100%;
min-width: 6.5em;
display: inline-block;
max-width: 100%;
/* min-width: 6.5em; */
}

input:not([type="radio"]) {
width: 33rem;
max-width: 100%;
margin-bottom: 0.5em;
min-height: 1.5em;
input:not([type='radio']) {
width: 20rem;
max-width: 100%;
margin-bottom: 0.5em;
min-height: 1.5em;
}

106 changes: 5 additions & 101 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,8 @@
import 'https://cdn.jsdelivr.net/npm/@radio4000/components/dist/index.min.js'
import 'https://cdn.jsdelivr.net/gh/oskarrough/rough-spinner/rough-spinner.js'
import { html, render } from 'https://unpkg.com/lit-html?module'
import { getAccessToken, extractSpotifyPlaylistId, getSpotifyPlaylist, parseSpotifyTrack, searchYoutube } from './helpers.js'

import SpotifyToYoutube from './spotify-to-youtube.js'
import R4BatchImport from './r4-batch-import.js'

// Get and set the Spotify access token
document.querySelector('#spotifyToYoutube').addEventListener('submit', handleSpotifyTokenSubmit)

export async function handleSpotifyTokenSubmit(event) {
event.preventDefault()

const $form = event.target
const formData = new FormData($form)

const $btn = $form.querySelector('button[type="submit"]')
const $token = $form.querySelector('[name="token"]')
$btn.disabled = true

// Get the Spotify token
let token = formData.get('token')
if (!token) {
try {
const clientId = formData.get('clientId')
const clientSecret = formData.get('clientSecret')
token = await getAccessToken(clientId, clientSecret)
$token.value = token
} catch (error) {
console.error('An error occurred:', error)
$token.value = 'ERROR GETTING TOKEN!'
}
}

try {
// Query the playlist
const playlistId = extractSpotifyPlaylistId(formData.get('url'))
const playlist = await getSpotifyPlaylist(playlistId, token)
const tracks = playlist.map(item => parseSpotifyTrack(item.track)).slice(0,3)

// Search YouTube and render as results come in
const maxResults = 4 //formData.get('limit')
for (const [i, t] of Object.entries(tracks)) {
tracks[i].searchResults = await searchYoutube(t.artist, t.title, t.isrc, maxResults)
displayResults(tracks, i)
}
console.log('Spotify tracks with YouTube search results', tracks)
} catch (error) {
console.error('An error occurred:', error)
} finally {
$btn.disabled = false
}
}

function displayResults(tracks, i) {
render(tableTemplate(tracks, Number(i) + 1), document.querySelector('#app'))
}

const tableTemplate = (tracks, i) => html`
${i < tracks.length ? html`<rough-spinner spinner="1" fps="30"></rough-spinner> Matching ${i}/${tracks.length}...` : null}
<form @submit=${saveResults}>
<ul class="tracks">
${tracks.map(
(track, i) => html`<li>
<strong>${i}. ${track.artist} - ${track.title}</strong>
<a target="_blank" href=${track.url}>link</a>
<ul class="results">
${track.searchResults.map((video) => searchResultTemplate(track, video))}
</ul>
</li>`
)}
</ul>
<button type="submit">Save results</button><br>
<br>
<textarea></textarea>
</form>
`

const searchResultTemplate = (track, video) => html`
<li>
<label>
<input type="radio" name=${'result_' + track.id} value=${video.id}>
<img src=${video.thumbnail} alt=${video.title} />
</label>
<ul>
<li><a href=${`https://www.youtube.com/watch?v=` + video.id} target="_blank">${video.title}</a></li>
${video.description ? html`<li>${video.description}</li>` : ''}
<li><small>
${video.channelTitle ? html`${video.channelTitle}, ` : ''}
${video.views}${video.publishedAt ? html`, ${video.publishedAt}` : ''}</small></li>
</ul>
</li>
`

// Inserts a newline with the YouTube URL for every matched track
function saveResults(event) {
event.preventDefault()
const fd = new FormData(event.target)
const youtubeIds = []
for (const [_key, ytid] of fd.entries()) {
youtubeIds.push(ytid)
}
const $output = event.target.querySelector('textarea')
$output.value = youtubeIds.map(id => `https://www.youtube.com/watch?v=${id}`).join('\r\n')
}

customElements.define('spotify-to-youtube', SpotifyToYoutube)
customElements.define('r4-batch-import', R4BatchImport)
77 changes: 77 additions & 0 deletions src/r4-batch-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { LitElement, html } from 'https://unpkg.com/lit?module'
import { sdk } from 'https://cdn.jsdelivr.net/npm/@radio4000/sdk@latest/+esm'

/**
* @typedef {Object} Match
* @property {string} spotifyId
* @property {string} youtubeId
* @property {string} title
* @property {string} url
*/

export default class R4BatchImport extends LitElement {
static get properties() {
return {
matches: { type: Array },
channel: { type: Object, state: true },
}
}

async connectedCallback() {
super.connectedCallback()
const user = await sdk.users.readUser()
if (user) this.setChannel()
}

async onSignIn({ detail }) {
if (detail.error) throw new Error('Could not sign in')
console.log(detail)
this.setChannel()
}

async setChannel() {
const { data: channels } = await sdk.channels.readUserChannels()
this.channel = channels[0] || null
}

logout() {
sdk.auth.signOut()
this.channel = undefined
}

async submit(event) {
this.loading = true
event.preventDefault()
console.log('import', this.matches)
for (const x of this.matches) {
await sdk.tracks.createTrack(this.channel.id, {
url: x.url,
title: x.title,
})
}
this.loading = false
}

render() {
return html`
${this.channel
? html`
${this.matches?.length
? html`
<p>
Ready to import ${this.matches?.length} tracks (the ones above) to your Radio4000 channel:
<strong>${this.channel.name}</strong> (@${this.channel.slug})
</p>
<form @submit=${this.submit}>
<button type="submit" ?disabled=${this.loading}>Import</button>
</form>
<br />
`
: html`<p>Waiting for matches...</p>`}
<button @click=${this.logout}>Logout of R4</button>
`
: html`<p>Sign in to Radio4000 to import a bunch of tracks</p>
<r4-sign-in @submit=${this.onSignIn}></r4-sign-in>`}
`
}
}
Loading

0 comments on commit 74a786f

Please sign in to comment.