diff --git a/src/main.js b/src/main.js index 89ceee6..0dee7ca 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,9 @@ import 'https://cdn.jsdelivr.net/npm/@radio4000/components/dist/r4.js' import 'https://cdn.jsdelivr.net/gh/oskarrough/rough-spinner/rough-spinner.js' import SpotifyToYoutube from './spotify-to-youtube.js' +import TextToYoutube from './text-to-youtube.js' import R4BatchImport from './r4-batch-import.js' customElements.define('spotify-to-youtube', SpotifyToYoutube) +customElements.define('text-to-youtube', TextToYoutube) customElements.define('r4-batch-import', R4BatchImport) diff --git a/src/text-to-youtube.js b/src/text-to-youtube.js new file mode 100644 index 0000000..e14b5d9 --- /dev/null +++ b/src/text-to-youtube.js @@ -0,0 +1,206 @@ +import { LitElement, html } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js' +import { searchYoutube } from './helpers.js' + +export default class TextToYoutube extends LitElement { + static get properties() { + return { + inputLines: { type: Array, state: true }, + youtubeResults: { type: Array, state: true }, + loading: { type: Boolean, state: true }, + didConfirmYoutubeResults: { type: Boolean, state: true }, + error: { type: String }, + i: { type: Number }, + } + } + + maxSearchResults = 3 + + // Updates this.tracks + async findMatches(event) { + event.preventDefault() + this.loading = true + const $form = event.target + const formData = new FormData($form) + const lines = formData.get('text_playlist').trim().split('\n') + if (!lines?.length) throw new Error('Failed to parse your playlist') + this.inputLines = lines.map((line) => { + return { + id: self.crypto.randomUUID(), // to keep track of the track + title: line, + searchResults: [], // for later + } + }) + + // Search YouTube in parallel and render as results come in + await Promise.allSettled( + this.inputLines.map((track, i) => + searchYoutube(track.title, this.maxSearchResults) + .then((results) => { + this.i = i + this.inputLines[i].searchResults = results + }) + .catch((error) => { + console.error('An error occurred:', error) + this.error = error.message + }) + ) + ) + this.loading = false + console.log('updated inputLines', this.inputLines) + } + + confirmMatches(event) { + event.preventDefault() + this.saveMatchingVideos() + this.didConfirmMatches = true + console.log('confirmed matches') + } + + // Inserts a newline with the YouTube URL for every matched track + saveMatchingVideos() { + const fd = new FormData(document.querySelector('form#tracksform')) + const results = [] + for (const [id, youtubeId] of fd.entries()) { + const internalTrack = this.inputLines.find((t) => t.id === id) + const track = { ...internalTrack, youtubeId, url: 'https://www.youtube.com/watch?v=' + youtubeId } + results.push(track) + } + this.youtubeResults = results + console.log('saved matches', this.youtubeResults) + } + + clearMatches() { + this.inputLines = [] + this.youtubeResults = [] + } + + skipTrack(event, track) { + event.preventDefault() + this.inputLines = this.inputLines.filter((t) => t.id !== track.id) + localStorage.setItem('syr.tracks', JSON.stringify(this.inputLines)) + } + + render() { + return html` +
+
+ Step 1. Write the tracks you want +
+
+
+ +
+ ${this.error + ? html` +

Error! Could not fetch this playlist. Is it public?
${this.error}

+ ` + : null} +

+ Matching ${Number(this.i || 0) + 1}/${this.inputLines?.length}... +
+

+
+
+ +
+
+ Step 2. Confirm your YouTube tracks +

For each track decide which matching YouTube video to keep, or skip.

+ ${this.inputLines?.length + ? html`
+
    + ${this.inputLines?.map( + (track, i) => html` +
  • + + ${i}. ${track.artist} - ${track.title} + link +
      + ${track.searchResults.map((video, i) => + searchResultTemplate(track, i, video, this.youtubeResults) + )} +
    +
  • + ` + )} +
+

+ or + +

+
` + : ''} +
+
+ +
+
+ Results +

Here are the tracks you chose. Do with it as you please.

+ +

Copy paste as CSV

+ +

Copy paste the YouTube IDs

+ +

Copy paste the YouTube URLs

+ +
+
+ ` + } + + // Disable shadow dom + createRenderRoot() { + return this + } +} + +function selectedVideo(event) { + const top = event.target.closest('ul').parentElement.nextElementSibling?.offsetTop + if (top) window.scrollTo({ top, behaviour: 'smooth' }) +} + +const searchResultTemplate = (track, index, video, matches) => html` +
  • + + +
  • +`