Commit e9edcf3
2025-07-03 10:30:42
Changed files (23)
internal
playlist
wav
playlist
2023-02
2023-03
2023-08
2024-01
2024-02
2024-03
2024-04
2024-05
2024-08
2025-02
2025-03
2025-04
2025-05
2025-06
internal/playlist/playlist.go
@@ -0,0 +1,174 @@
+package playlist
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/channelmeter/iso8601duration"
+ "google.golang.org/api/option"
+ "google.golang.org/api/youtube/v3"
+)
+
+type Playlist struct {
+ Id string `json:"id"`
+ Title string `json:"title"`
+ Tracks []Track `json:"playlist"`
+}
+
+type Track struct {
+ Filename string `json:"file"`
+ TrimStart float64 `json:"trim_start,omitempty"`
+ TrimEnd float64 `json:"trim_end,omitempty"`
+ Fade float64 `json:"fade,omitempty"` // crossfade
+ Details *Details `json:"details,omitempty"` // YouTube metadata
+}
+
+type Details struct {
+ Title string `json:"title,omitempty"`
+ Artist string `json:"artist,omitempty"`
+ VideoID string `json:"video_id,omitempty"`
+ Duration string `json:"duration,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ ChannelId string `json:"channel_id,omitempty"`
+}
+
+var (
+ ErrPlaylistNotFound = errors.New("playlist not found")
+)
+
+func newTrackFromPlaylistItem(item *youtube.PlaylistItem) Track {
+
+ sn := item.Snippet
+
+ channel := sn.VideoOwnerChannelTitle
+ if strings.HasSuffix(channel, " - Topic") {
+ channel = strings.TrimSuffix(channel, " - Topic")
+ }
+
+ // get the highset resolution returned
+ tn := ""
+ switch {
+ case sn.Thumbnails.Maxres != nil:
+ tn = sn.Thumbnails.Maxres.Url
+ case sn.Thumbnails.High != nil:
+ tn = sn.Thumbnails.High.Url
+ case sn.Thumbnails.Medium != nil:
+ tn = sn.Thumbnails.Medium.Url
+ case sn.Thumbnails.Default != nil:
+ tn = sn.Thumbnails.Default.Url
+ }
+
+ return Track{
+ Filename: sn.ResourceId.VideoId + ".wav",
+ Details: &Details{
+ Title: sn.Title,
+ VideoID: sn.ResourceId.VideoId,
+ Thumbnail: tn,
+ Artist: channel,
+ ChannelId: sn.VideoOwnerChannelId,
+ },
+ }
+}
+
+func formatDuration(raw string) string {
+ dur, err := duration.FromString(raw)
+ if err != nil {
+ return "0:00"
+ }
+ d := dur.ToDuration()
+ mins := int(d.Minutes())
+ secs := int(d.Seconds()) % 60
+ return fmt.Sprintf("%d:%02d", mins, secs)
+}
+
+func LookupPlaylist(apiKey, playlistID string) (*Playlist, error) {
+
+ const (
+ _batchSize = 50
+ )
+
+ ctx := context.Background()
+ service, err := youtube.NewService(ctx, option.WithAPIKey(apiKey))
+ if err != nil {
+ return nil, fmt.Errorf("setting up youtube api client: %w", err)
+ }
+
+ playlistCall := service.Playlists.
+ List([]string{"snippet"}).
+ Id(playlistID)
+ playlistResp, err := playlistCall.Do()
+ if err != nil {
+ err = fmt.Errorf("fetching playlist details: %w", err)
+ return nil, err
+ }
+ if len(playlistResp.Items) == 0 {
+ err = fmt.Errorf("playlist %q: %w",
+ playlistID,
+ ErrPlaylistNotFound)
+ return nil, err
+ }
+
+ pl := &Playlist{
+ Id: playlistID,
+ Title: playlistResp.Items[0].Snippet.Title,
+ Tracks: make([]Track, 0),
+ }
+
+ itemsCall := service.PlaylistItems.
+ List([]string{"snippet"}).
+ PlaylistId(playlistID).
+ MaxResults(_batchSize)
+
+ // map tracks by Id while keeping them in their Playlist
+ trackMap := make(map[string]*Track)
+
+ // build trackList
+ for {
+ resp, err := itemsCall.Do()
+ if err != nil {
+ err = fmt.Errorf("fetching playlist items: %w", err)
+ return nil, err
+ }
+
+ for _, item := range resp.Items {
+ track := newTrackFromPlaylistItem(item)
+ pl.Tracks = append(pl.Tracks, track)
+ trackMap[track.Details.VideoID] = &pl.Tracks[len(pl.Tracks)-1]
+ }
+
+ if resp.NextPageToken == "" {
+ break
+ }
+ itemsCall.PageToken(resp.NextPageToken)
+ }
+
+ for i := 0; i < len(pl.Tracks); i = i + _batchSize {
+
+ end := min(i+_batchSize, len(pl.Tracks))
+ idSlice := make([]string, 0, end-i)
+ for _, t := range pl.Tracks[i:end] {
+ idSlice = append(idSlice, t.Details.VideoID)
+ }
+ ids := strings.Join(idSlice, ",")
+
+ resp, err := service.Videos.
+ List([]string{"contentDetails"}).
+ Id(ids).
+ Do()
+ if err != nil {
+ err = fmt.Errorf("fetching video details: %w", err)
+ return nil, err
+ }
+
+ for _, item := range resp.Items {
+ t, trackFound := trackMap[item.Id]
+ if trackFound {
+ t.Details.Duration = formatDuration(item.ContentDetails.Duration)
+ }
+ }
+
+ }
+ return pl, nil
+}
internal/wav/wav.go
@@ -0,0 +1,265 @@
+package wav
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "slices"
+)
+
+type WAV struct {
+ SampleRate int
+ Channels int
+ BitsPerSample int
+ PCM []int16
+}
+
+var (
+ ErrNotWAV = errors.New("invalid WAV file")
+ ErrIncompatibleWAV = errors.New("incompatible WAV files")
+)
+
+const (
+ _bytesPerSample = 2 // int16 PCM
+)
+
+func findDataChunk(r io.ReadSeeker) (int, error) {
+ // Seek to offset 12 to start scanning chunks after RIFF header
+ _, err := r.Seek(12, io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+
+ var chunkHeader [8]byte
+ for {
+ _, err := io.ReadFull(r, chunkHeader[:])
+ if err != nil {
+ return 0, err
+ }
+ chunkID := string(chunkHeader[0:4])
+ chunkSize := int(binary.LittleEndian.Uint32(chunkHeader[4:8]))
+
+ if chunkID == "data" {
+ return chunkSize, nil
+ }
+
+ // Skip this chunk
+ _, err = r.Seek(int64(chunkSize+chunkSize%2), io.SeekCurrent) // word-align chunks
+ if err != nil {
+ return 0, err
+ }
+ }
+}
+
+func ReadWAV(path string) (*WAV, error) {
+
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("opening wav: %w", err)
+ }
+ defer f.Close()
+
+ header := make([]byte, 44)
+
+ _, err = io.ReadFull(f, header)
+ if err != nil {
+ return nil, fmt.Errorf("reading wav header: %w", err)
+ }
+
+ magic := string(header[0:4])
+ if magic != "RIFF" && magic != "WAVE" {
+ return nil, fmt.Errorf("magic=%q: %w", magic, ErrNotWAV)
+ }
+
+ channels := int(binary.LittleEndian.Uint16(header[22:24]))
+ sampleRate := int(binary.LittleEndian.Uint32(header[24:28]))
+ // TODO: whats at 28:34?
+ bitsPerSample := int(binary.LittleEndian.Uint16(header[34:36]))
+ // TODO: whats at 36:40?
+ dataLen, err := findDataChunk(f)
+ if err != nil {
+ return nil, fmt.Errorf("finding data chunk: %w", err)
+ }
+ sampleCount := dataLen / _bytesPerSample
+ pcm := make([]int16, sampleCount)
+
+ err = binary.Read(f, binary.LittleEndian, pcm)
+ if err != nil {
+ return nil, fmt.Errorf("reading wav data: %w", err)
+ }
+
+ return &WAV{
+ SampleRate: sampleRate,
+ Channels: channels,
+ BitsPerSample: bitsPerSample,
+ PCM: pcm,
+ }, nil
+}
+
+func (w *WAV) WriteFile(path string) error {
+ out, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ byteRate := w.SampleRate * w.Channels * _bytesPerSample
+ blockAlign := w.Channels * _bytesPerSample
+ dataSize := len(w.PCM) * _bytesPerSample
+ riffSize := 36 + dataSize
+
+ header := make([]byte, 44)
+ copy(header[0:], []byte("RIFF"))
+ binary.LittleEndian.PutUint32(header[4:], uint32(riffSize))
+ copy(header[8:], []byte("WAVEfmt "))
+ binary.LittleEndian.PutUint32(header[16:], 16) // fmt chunk size
+ binary.LittleEndian.PutUint16(header[20:], 1) // PCM format
+ binary.LittleEndian.PutUint16(header[22:], uint16(w.Channels))
+ binary.LittleEndian.PutUint32(header[24:], uint32(w.SampleRate))
+ binary.LittleEndian.PutUint32(header[28:], uint32(byteRate))
+ binary.LittleEndian.PutUint16(header[32:], uint16(blockAlign))
+ binary.LittleEndian.PutUint16(header[34:], uint16(w.BitsPerSample))
+ copy(header[36:], []byte("data"))
+ binary.LittleEndian.PutUint32(header[40:], uint32(dataSize))
+
+ _, err = out.Write(header)
+ if err != nil {
+ return err
+ }
+ return binary.Write(out, binary.LittleEndian, w.PCM)
+}
+
+func check(first, second *WAV) error {
+ if first.SampleRate != second.SampleRate {
+ return fmt.Errorf("sample rate a=%d b=%d: %w",
+ first.SampleRate,
+ second.SampleRate,
+ ErrIncompatibleWAV)
+ }
+ if first.BitsPerSample != second.BitsPerSample {
+ return fmt.Errorf("bits per sample a=%d b=%d: %w",
+ first.BitsPerSample,
+ second.BitsPerSample,
+ ErrIncompatibleWAV)
+ }
+ if first.Channels != second.Channels {
+ return fmt.Errorf("channels a=%d b=%d: %w",
+ first.Channels,
+ second.Channels,
+ ErrIncompatibleWAV)
+ }
+ return nil
+}
+
+func (first *WAV) Append(second *WAV) (*WAV, error) {
+ err := check(first, second)
+ if err != nil {
+ return nil, err
+ }
+
+ // transition prep and bounds checking
+ reviewDuration := 5
+ reviewSamples := first.SampleRate * first.Channels * reviewDuration
+ if len(first.PCM) < reviewSamples {
+ reviewSamples = len(first.PCM)
+ }
+ if len(second.PCM) < reviewSamples {
+ reviewSamples = len(second.PCM)
+ }
+
+ reviewPCM := append(first.PCM[len(first.PCM)-reviewSamples:], second.PCM[:reviewSamples]...)
+ first.PCM = append(first.PCM, second.PCM...)
+
+ return &WAV{
+ SampleRate: first.SampleRate,
+ Channels: first.Channels,
+ BitsPerSample: first.BitsPerSample,
+ PCM: reviewPCM,
+ }, nil
+}
+
+func (first *WAV) Fade(second *WAV, d float64) (*WAV, error) {
+ err := check(first, second)
+ if err != nil {
+ return nil, err
+ }
+
+ fadeSamples := int(d * float64(first.SampleRate*first.Channels))
+ if fadeSamples <= 0 || fadeSamples > len(first.PCM) || fadeSamples > len(second.PCM) {
+ return first.Append(second)
+ }
+
+ fadeStart := len(first.PCM) - fadeSamples
+ transition := make([]int16, fadeSamples)
+ for i := range fadeSamples {
+ fadeIn := float64(i) / float64(fadeSamples)
+ fadeOut := 1.0 - fadeIn
+ sampleA := float64(first.PCM[fadeStart+i])
+ sampleB := float64(second.PCM[i])
+ transition[i] = int16(sampleA*fadeOut + sampleB*fadeIn)
+ }
+
+ first.PCM = slices.Concat(
+ first.PCM[:fadeStart],
+ transition,
+ second.PCM[fadeSamples:],
+ )
+
+ reviewDuration := 2
+ reviewSamples := first.SampleRate * first.Channels * reviewDuration
+ reviewStart := fadeStart - reviewSamples
+ reviewEnd := fadeStart + fadeSamples + reviewSamples
+ reviewPCM := first.PCM[reviewStart:reviewEnd]
+
+ return &WAV{
+ SampleRate: first.SampleRate,
+ Channels: first.Channels,
+ BitsPerSample: first.BitsPerSample,
+ PCM: reviewPCM,
+ }, nil
+}
+
+func (w *WAV) Stretch(count int) {
+ ch := w.Channels
+ frames := len(w.PCM) / ch
+ stretched := make([]int16, 0, len(w.PCM)*count)
+
+ for i := range frames {
+ frame := w.PCM[i*ch : (i+1)*ch]
+ for range count {
+ stretched = append(stretched, frame...)
+ }
+ }
+
+ w.PCM = stretched
+}
+
+func (w *WAV) TrimEnd(d float64) {
+ if d <= 0 {
+ return
+ }
+
+ trimSamples := int(d * float64(w.SampleRate*w.Channels))
+ newEnd := len(w.PCM) - trimSamples
+ if newEnd < 0 {
+ newEnd = 0
+ }
+
+ w.PCM = w.PCM[:newEnd]
+}
+
+func (w *WAV) TrimStart(d float64) {
+ if d <= 0 {
+ return
+ }
+
+ trimSamples := int(d * float64(w.SampleRate*w.Channels))
+ newStart := len(w.PCM) - trimSamples
+ if newStart < 0 {
+ newStart = 0
+ }
+
+ w.PCM = w.PCM[newStart:]
+}
playlist/2023-02/playlist.json
@@ -0,0 +1,160 @@
+{
+ "id": "PL1q1SH2wELHAMuhrjJ9b0QB-cb6dyirpj",
+ "title": "singles - 2023-02",
+ "playlist": [
+ {
+ "file": "HkEbEYoAwe4.wav",
+ "details": {
+ "title": "Over",
+ "artist": "CHVRCHES",
+ "video_id": "HkEbEYoAwe4",
+ "duration": "3:38",
+ "thumbnail": "https://i.ytimg.com/vi/HkEbEYoAwe4/maxresdefault.jpg",
+ "channel_id": "UCvInFYiyeAJOGEjhqJnyaMA"
+ }
+ },
+ {
+ "file": "iqs5th3xiBg.wav",
+ "details": {
+ "title": "Die 4 Me",
+ "artist": "Halsey",
+ "video_id": "iqs5th3xiBg",
+ "duration": "3:36",
+ "thumbnail": "https://i.ytimg.com/vi/iqs5th3xiBg/maxresdefault.jpg",
+ "channel_id": "UC3BBS0-pODIeS8QGX_qtrCg"
+ }
+ },
+ {
+ "file": "1HkVbF-Z1gA.wav",
+ "details": {
+ "title": "Can’t Let You Go",
+ "artist": "LP Giobbi",
+ "video_id": "1HkVbF-Z1gA",
+ "duration": "2:53",
+ "thumbnail": "https://i.ytimg.com/vi/1HkVbF-Z1gA/maxresdefault.jpg",
+ "channel_id": "UCoVxBu47k0Sgq3ybXxw6jSA"
+ }
+ },
+ {
+ "file": "COGWtzclJ1c.wav",
+ "details": {
+ "title": "Land Locked Heart (from Road 96: Mile 0)",
+ "artist": "The Midnight",
+ "video_id": "COGWtzclJ1c",
+ "duration": "3:24",
+ "thumbnail": "https://i.ytimg.com/vi/COGWtzclJ1c/maxresdefault.jpg",
+ "channel_id": "UCkhnqwH8DtT0Ap3A-mi_GJQ"
+ }
+ },
+ {
+ "file": "68znszXi-No.wav",
+ "details": {
+ "title": "New Order T-Shirt",
+ "artist": "The National",
+ "video_id": "68znszXi-No",
+ "duration": "4:56",
+ "thumbnail": "https://i.ytimg.com/vi/68znszXi-No/maxresdefault.jpg",
+ "channel_id": "UC1bWXfKY5YoBduVCcSP8SGQ"
+ }
+ },
+ {
+ "file": "5HamkAvvrSs.wav",
+ "details": {
+ "title": "Foxglove Through The Clearcut (Acoustic)",
+ "artist": "Death Cab for Cutie",
+ "video_id": "5HamkAvvrSs",
+ "duration": "5:06",
+ "thumbnail": "https://i.ytimg.com/vi/5HamkAvvrSs/maxresdefault.jpg",
+ "channel_id": "UCQyYgbGkyvK3v3X1XzGDQ2Q"
+ }
+ },
+ {
+ "file": "UCWN8u2-8RA.wav",
+ "details": {
+ "title": "Body Better (Acoustic)",
+ "artist": "Maisie Peters",
+ "video_id": "UCWN8u2-8RA",
+ "duration": "3:16",
+ "thumbnail": "https://i.ytimg.com/vi/UCWN8u2-8RA/maxresdefault.jpg",
+ "channel_id": "UCh1mOseeXbq8xWuEAsWhoAg"
+ }
+ },
+ {
+ "file": "9-7vmrxLFf0.wav",
+ "details": {
+ "title": "Love Who We Are Meant To",
+ "artist": "Feist",
+ "video_id": "9-7vmrxLFf0",
+ "duration": "3:56",
+ "thumbnail": "https://i.ytimg.com/vi/9-7vmrxLFf0/maxresdefault.jpg",
+ "channel_id": "UCpqoOPpIfyUu6qCh-i-lUtQ"
+ }
+ },
+ {
+ "file": "dBqVO53J4rQ.wav",
+ "details": {
+ "title": "Fucked It Up",
+ "artist": "City And Colour",
+ "video_id": "dBqVO53J4rQ",
+ "duration": "3:59",
+ "thumbnail": "https://i.ytimg.com/vi/dBqVO53J4rQ/maxresdefault.jpg",
+ "channel_id": "UC9rTPiSAfswoLoqCJThN1Zg"
+ }
+ },
+ {
+ "file": "7J8xRLXVyJg.wav",
+ "details": {
+ "title": "Hopeful",
+ "artist": "ODESZA",
+ "video_id": "7J8xRLXVyJg",
+ "duration": "4:09",
+ "thumbnail": "https://i.ytimg.com/vi/7J8xRLXVyJg/maxresdefault.jpg",
+ "channel_id": "UCqbIVM8TSSWcYmImtAltzlQ"
+ }
+ },
+ {
+ "file": "WsMYFXHY3MM.wav",
+ "details": {
+ "title": "Still Night Air",
+ "artist": "plenka",
+ "video_id": "WsMYFXHY3MM",
+ "duration": "3:03",
+ "thumbnail": "https://i.ytimg.com/vi/WsMYFXHY3MM/maxresdefault.jpg",
+ "channel_id": "UCnQHLFKsCRzyYTLJgA3Ud8Q"
+ }
+ },
+ {
+ "file": "pEjIv5ngnc0.wav",
+ "details": {
+ "title": "Shattered Dreams",
+ "artist": "METAHESH",
+ "video_id": "pEjIv5ngnc0",
+ "duration": "2:15",
+ "thumbnail": "https://i.ytimg.com/vi/pEjIv5ngnc0/maxresdefault.jpg",
+ "channel_id": "UCKrbqdPY0rtT6DCKno5Aorw"
+ }
+ },
+ {
+ "file": "6N6JGCh1MY4.wav",
+ "details": {
+ "title": "We Are The People (southstar Remix)",
+ "artist": "Empire of The Sun",
+ "video_id": "6N6JGCh1MY4",
+ "duration": "3:57",
+ "thumbnail": "https://i.ytimg.com/vi/6N6JGCh1MY4/maxresdefault.jpg",
+ "channel_id": "UC39pUcAh8ipNPkA1W0dR0cA"
+ }
+ },
+ {
+ "file": "PBhSay4wHV4.wav",
+ "details": {
+ "title": "slow breaths",
+ "artist": "No Spirit",
+ "video_id": "PBhSay4wHV4",
+ "duration": "3:45",
+ "thumbnail": "https://i.ytimg.com/vi/PBhSay4wHV4/maxresdefault.jpg",
+ "channel_id": "UCraRksyJpUyJQexB7-hdoTQ"
+ }
+ }
+ ]
+}
playlist/2023-03/playlist.json
@@ -0,0 +1,127 @@
+{
+ "id": "PL1q1SH2wELHBi0hJZTCNjF5sOKb4oa2Q2",
+ "title": "singles - 2023-03",
+ "playlist": [
+ {
+ "file": "krXfMlK6sS8.wav",
+ "details": {
+ "title": "Raincatchers",
+ "artist": "Birdy",
+ "video_id": "krXfMlK6sS8",
+ "duration": "4:00",
+ "thumbnail": "https://i.ytimg.com/vi/krXfMlK6sS8/maxresdefault.jpg",
+ "channel_id": "UC8v5OGljUo7ATs_14mm-aQw"
+ }
+ },
+ {
+ "file": "CwGGJLLtdYM.wav",
+ "details": {
+ "title": "After Midnight (feat. Clairo)",
+ "artist": "Phoenix",
+ "video_id": "CwGGJLLtdYM",
+ "duration": "3:08",
+ "thumbnail": "https://i.ytimg.com/vi/CwGGJLLtdYM/maxresdefault.jpg",
+ "channel_id": "UCAoGVyUtK3AHYwjJ0xDFbFw"
+ }
+ },
+ {
+ "file": "K8N31Gh_IrQ.wav",
+ "details": {
+ "title": "To Be Yours (feat. Claud)",
+ "artist": "ODESZA",
+ "video_id": "K8N31Gh_IrQ",
+ "duration": "3:32",
+ "thumbnail": "https://i.ytimg.com/vi/K8N31Gh_IrQ/maxresdefault.jpg",
+ "channel_id": "UCqbIVM8TSSWcYmImtAltzlQ"
+ }
+ },
+ {
+ "file": "uzZkhPDoVyw.wav",
+ "details": {
+ "title": "2gether",
+ "artist": "Mura Masa",
+ "video_id": "uzZkhPDoVyw",
+ "duration": "3:12",
+ "thumbnail": "https://i.ytimg.com/vi/uzZkhPDoVyw/maxresdefault.jpg",
+ "channel_id": "UCEYapch47TYc3ldsQIrC3Uw"
+ }
+ },
+ {
+ "file": "deBzLAFYp7g.wav",
+ "details": {
+ "title": "lovespell",
+ "artist": "Covet",
+ "video_id": "deBzLAFYp7g",
+ "duration": "4:12",
+ "thumbnail": "https://i.ytimg.com/vi/deBzLAFYp7g/maxresdefault.jpg",
+ "channel_id": "UCEtONSxjR7NSOMdrEfjF-eA"
+ }
+ },
+ {
+ "file": "lBpF2bwpOzs.wav",
+ "details": {
+ "title": "Don't Fade Away",
+ "artist": "Beach Fossils",
+ "video_id": "lBpF2bwpOzs",
+ "duration": "3:23",
+ "thumbnail": "https://i.ytimg.com/vi/lBpF2bwpOzs/maxresdefault.jpg",
+ "channel_id": "UCa3Tz2g-TF1oumK2paiC9yg"
+ }
+ },
+ {
+ "file": "WjTxOa_HxQo.wav",
+ "details": {
+ "title": "Weekends (Chill Mix)",
+ "artist": "Freya Ridings",
+ "video_id": "WjTxOa_HxQo",
+ "duration": "3:21",
+ "thumbnail": "https://i.ytimg.com/vi/WjTxOa_HxQo/maxresdefault.jpg",
+ "channel_id": "UCw5G4AVjJ_YI9BOTjj-v1iw"
+ }
+ },
+ {
+ "file": "T7uu6AI_a3o.wav",
+ "details": {
+ "title": "All We've Ever Wanted",
+ "artist": "Palace",
+ "video_id": "T7uu6AI_a3o",
+ "duration": "3:04",
+ "thumbnail": "https://i.ytimg.com/vi/T7uu6AI_a3o/maxresdefault.jpg",
+ "channel_id": "UCxMXIrwJ_ZHiSEjkj4vdSxw"
+ }
+ },
+ {
+ "file": "-Zvwi-rWYSY.wav",
+ "details": {
+ "title": "The Descent",
+ "artist": "Blue Wednesday",
+ "video_id": "-Zvwi-rWYSY",
+ "duration": "3:34",
+ "thumbnail": "https://i.ytimg.com/vi/-Zvwi-rWYSY/maxresdefault.jpg",
+ "channel_id": "UCH5Ryy6Kqv89TFDBS6T4bNg"
+ }
+ },
+ {
+ "file": "qNnokjRg1_k.wav",
+ "details": {
+ "title": "El Bueno Y El Malo",
+ "artist": "Hermanos Gutiérrez",
+ "video_id": "qNnokjRg1_k",
+ "duration": "3:26",
+ "thumbnail": "https://i.ytimg.com/vi/qNnokjRg1_k/maxresdefault.jpg",
+ "channel_id": "UC80iwcArxltADbfXSS9oGCQ"
+ }
+ },
+ {
+ "file": "Mdrbs5TC-r0.wav",
+ "details": {
+ "title": "Awake",
+ "artist": "Softy",
+ "video_id": "Mdrbs5TC-r0",
+ "duration": "2:38",
+ "thumbnail": "https://i.ytimg.com/vi/Mdrbs5TC-r0/maxresdefault.jpg",
+ "channel_id": "UCtjbjXoaBzpZhprRKI-7r7A"
+ }
+ }
+ ]
+}
playlist/2023-08/playlist.json
@@ -0,0 +1,127 @@
+{
+ "id": "PL1q1SH2wELHCxogyVETO3ON21FqTHn-Np",
+ "title": "singles - 2023-08",
+ "playlist": [
+ {
+ "file": "l9JQ02yrsIQ.wav",
+ "details": {
+ "title": "Just The Once",
+ "artist": "Metric",
+ "video_id": "l9JQ02yrsIQ",
+ "duration": "3:25",
+ "thumbnail": "https://i.ytimg.com/vi/l9JQ02yrsIQ/maxresdefault.jpg",
+ "channel_id": "UCEDAqx7QPrOUV3Bx9aqUjsw"
+ }
+ },
+ {
+ "file": "mK4q1DuqqRQ.wav",
+ "details": {
+ "title": "Manhattan",
+ "artist": "CHVRCHES",
+ "video_id": "mK4q1DuqqRQ",
+ "duration": "5:20",
+ "thumbnail": "https://i.ytimg.com/vi/mK4q1DuqqRQ/maxresdefault.jpg",
+ "channel_id": "UCvInFYiyeAJOGEjhqJnyaMA"
+ }
+ },
+ {
+ "file": "9B9KSCCsrzs.wav",
+ "details": {
+ "title": "Medicine",
+ "artist": "Nolie",
+ "video_id": "9B9KSCCsrzs",
+ "duration": "3:34",
+ "thumbnail": "https://i.ytimg.com/vi/9B9KSCCsrzs/maxresdefault.jpg",
+ "channel_id": "UCc4v1KaB0-2RkGfasS9qDDQ"
+ }
+ },
+ {
+ "file": "rdtEJVjBkBI.wav",
+ "details": {
+ "title": "NYE",
+ "artist": "Local Natives",
+ "video_id": "rdtEJVjBkBI",
+ "duration": "2:56",
+ "thumbnail": "https://i.ytimg.com/vi/rdtEJVjBkBI/maxresdefault.jpg",
+ "channel_id": "UCZGPl54hjSOMbVgCAJfpEeg"
+ }
+ },
+ {
+ "file": "3YTJQaSp4Ks.wav",
+ "details": {
+ "title": "NO PLACE IS TOO FAR",
+ "artist": "San Holo",
+ "video_id": "3YTJQaSp4Ks",
+ "duration": "3:01",
+ "thumbnail": "https://i.ytimg.com/vi/3YTJQaSp4Ks/maxresdefault.jpg",
+ "channel_id": "UCEFIhOEN2vXu2WzlBzrwEoA"
+ }
+ },
+ {
+ "file": "Pl6TrmVHK_0.wav",
+ "details": {
+ "title": "Taste like Venom",
+ "artist": "GUNSHIP",
+ "video_id": "Pl6TrmVHK_0",
+ "duration": "3:27",
+ "thumbnail": "https://i.ytimg.com/vi/Pl6TrmVHK_0/maxresdefault.jpg",
+ "channel_id": "UCQr3Q0ixJWZB3s3K-Y1fgRw"
+ }
+ },
+ {
+ "file": "a7PmMY1HG4Y.wav",
+ "details": {
+ "title": "The Scenic Route",
+ "artist": "Toonorth",
+ "video_id": "a7PmMY1HG4Y",
+ "duration": "2:44",
+ "thumbnail": "https://i.ytimg.com/vi/a7PmMY1HG4Y/maxresdefault.jpg",
+ "channel_id": "UCvTQFhVL9NAxPtoody0X89A"
+ }
+ },
+ {
+ "file": "pHjXk2giLK0.wav",
+ "details": {
+ "title": "Read My Mind",
+ "artist": "Timecop1983",
+ "video_id": "pHjXk2giLK0",
+ "duration": "3:18",
+ "thumbnail": "https://i.ytimg.com/vi/pHjXk2giLK0/maxresdefault.jpg",
+ "channel_id": "UCVjNyglpxEwPoB65b0Oc4Ew"
+ }
+ },
+ {
+ "file": "HoyyXgTR5LI.wav",
+ "details": {
+ "title": "Loading",
+ "artist": "James Blake",
+ "video_id": "HoyyXgTR5LI",
+ "duration": "4:45",
+ "thumbnail": "https://i.ytimg.com/vi/HoyyXgTR5LI/maxresdefault.jpg",
+ "channel_id": "UCx90L12QXzc18AGYFRof-EA"
+ }
+ },
+ {
+ "file": "0LlJkLzW4ME.wav",
+ "details": {
+ "title": "So You Are Tired",
+ "artist": "Sufjan Stevens",
+ "video_id": "0LlJkLzW4ME",
+ "duration": "4:50",
+ "thumbnail": "https://i.ytimg.com/vi/0LlJkLzW4ME/maxresdefault.jpg",
+ "channel_id": "UC5sOhxfts379lOCXaIAinyw"
+ }
+ },
+ {
+ "file": "ALcpR7xc6q0.wav",
+ "details": {
+ "title": "Under the Sun",
+ "artist": "Spellling",
+ "video_id": "ALcpR7xc6q0",
+ "duration": "4:58",
+ "thumbnail": "https://i.ytimg.com/vi/ALcpR7xc6q0/maxresdefault.jpg",
+ "channel_id": "UCyTiDo_LravmqUJ5y1lqomg"
+ }
+ }
+ ]
+}
playlist/2024-01/playlist.json
@@ -0,0 +1,105 @@
+{
+ "id": "PL1q1SH2wELHArW0-4I00h7MNVAh5mw1Lw",
+ "title": "singles - 2024-01",
+ "playlist": [
+ {
+ "file": "_lhOYRjVDkI.wav",
+ "details": {
+ "title": "Let Time Pass",
+ "artist": "fallen down",
+ "video_id": "_lhOYRjVDkI",
+ "duration": "3:08",
+ "thumbnail": "https://i.ytimg.com/vi/_lhOYRjVDkI/maxresdefault.jpg",
+ "channel_id": "UCQtqlKMf_BOgxJBudrCQPfg"
+ }
+ },
+ {
+ "file": "x0a1N7Xe40c.wav",
+ "details": {
+ "title": "Ethical Plum (feat. BAYLI)",
+ "artist": "Chaos Chaos",
+ "video_id": "x0a1N7Xe40c",
+ "duration": "2:47",
+ "thumbnail": "https://i.ytimg.com/vi/x0a1N7Xe40c/maxresdefault.jpg",
+ "channel_id": "UCKucIcotC_GOPmYGdewF4PA"
+ }
+ },
+ {
+ "file": "RvV3-Juzhds.wav",
+ "details": {
+ "title": "Your Blood",
+ "artist": "AURORA",
+ "video_id": "RvV3-Juzhds",
+ "duration": "4:09",
+ "thumbnail": "https://i.ytimg.com/vi/RvV3-Juzhds/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "0vi6H7tZs90.wav",
+ "details": {
+ "title": "All We'll Ever Be",
+ "artist": "Mint Julep",
+ "video_id": "0vi6H7tZs90",
+ "duration": "5:17",
+ "thumbnail": "https://i.ytimg.com/vi/0vi6H7tZs90/maxresdefault.jpg",
+ "channel_id": "UCSaPP3FUdsecX6gnkbI-Gug"
+ }
+ },
+ {
+ "file": "-mnHen0g54M.wav",
+ "details": {
+ "title": "Wicked Game",
+ "artist": "Trevor Something",
+ "video_id": "-mnHen0g54M",
+ "duration": "4:59",
+ "thumbnail": "https://i.ytimg.com/vi/-mnHen0g54M/maxresdefault.jpg",
+ "channel_id": "UC833__IIxnI8u737Y1V6a5g"
+ }
+ },
+ {
+ "file": "QGeOAk9B9Ig.wav",
+ "details": {
+ "title": "Bleach",
+ "artist": "Palace",
+ "video_id": "QGeOAk9B9Ig",
+ "duration": "4:10",
+ "thumbnail": "https://i.ytimg.com/vi/QGeOAk9B9Ig/maxresdefault.jpg",
+ "channel_id": "UCxMXIrwJ_ZHiSEjkj4vdSxw"
+ }
+ },
+ {
+ "file": "Jo_Sbc878hI.wav",
+ "details": {
+ "title": "Revelator",
+ "artist": "Phosphorescent",
+ "video_id": "Jo_Sbc878hI",
+ "duration": "4:54",
+ "thumbnail": "https://i.ytimg.com/vi/Jo_Sbc878hI/maxresdefault.jpg",
+ "channel_id": "UCxZ-f93PiINbcJM4jURCg5w"
+ }
+ },
+ {
+ "file": "FcXRwUsA6xs.wav",
+ "details": {
+ "title": "What Was I Made For? [From The Motion Picture \"Barbie\"]",
+ "artist": "Billie Eilish",
+ "video_id": "FcXRwUsA6xs",
+ "duration": "3:43",
+ "thumbnail": "https://i.ytimg.com/vi/FcXRwUsA6xs/maxresdefault.jpg",
+ "channel_id": "UCERrDZ8oN0U_n9MphMKERcg"
+ }
+ },
+ {
+ "file": "U9FcPt2rLI4.wav",
+ "details": {
+ "title": "The Conflict of the Mind",
+ "artist": "AURORA",
+ "video_id": "U9FcPt2rLI4",
+ "duration": "4:16",
+ "thumbnail": "https://i.ytimg.com/vi/U9FcPt2rLI4/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ }
+ ]
+}
playlist/2024-02/playlist.json
@@ -0,0 +1,112 @@
+{
+ "id": "PL1q1SH2wELHAeAUTVYQj6TZr15Xg4yJie",
+ "title": "singles - 2024-02",
+ "playlist": [
+ {
+ "file": "tXdBlULLSaU.wav",
+ "details": {
+ "title": "Bored",
+ "artist": "Waxahatchee",
+ "video_id": "tXdBlULLSaU",
+ "duration": "2:56",
+ "thumbnail": "https://i.ytimg.com/vi/tXdBlULLSaU/maxresdefault.jpg",
+ "channel_id": "UCcOvXhB6k11aVrsT05oeqqg"
+ }
+ },
+ {
+ "file": "bKaRxvxeUzs.wav",
+ "details": {
+ "title": "Daydream Repeat",
+ "artist": "Four Tet",
+ "video_id": "bKaRxvxeUzs",
+ "duration": "6:09",
+ "thumbnail": "https://i.ytimg.com/vi/bKaRxvxeUzs/maxresdefault.jpg",
+ "channel_id": "UCd6q-S_HAePombe72bczCkA"
+ }
+ },
+ {
+ "file": "6OJZVUoHt9c.wav",
+ "details": {
+ "title": "Deleted video",
+ "video_id": "6OJZVUoHt9c"
+ }
+ },
+ {
+ "file": "tJmRIpeS5w0.wav",
+ "details": {
+ "title": "Older",
+ "artist": "Lizzy McAlpine",
+ "video_id": "tJmRIpeS5w0",
+ "duration": "3:22",
+ "thumbnail": "https://i.ytimg.com/vi/tJmRIpeS5w0/maxresdefault.jpg",
+ "channel_id": "UCC1TOSYNYXw1xrwNRxW46dg"
+ }
+ },
+ {
+ "file": "30fZpGsTL3o.wav",
+ "details": {
+ "title": "Florescence",
+ "artist": "Narvent",
+ "video_id": "30fZpGsTL3o",
+ "duration": "3:01",
+ "thumbnail": "https://i.ytimg.com/vi/30fZpGsTL3o/maxresdefault.jpg",
+ "channel_id": "UCf0MXejn3VTMr1tvPFszwOg"
+ }
+ },
+ {
+ "file": "WzpxaqAtYqs.wav",
+ "details": {
+ "title": "GLOW (feat. Au/Ra)",
+ "artist": "San Holo",
+ "video_id": "WzpxaqAtYqs",
+ "duration": "2:31",
+ "thumbnail": "https://i.ytimg.com/vi/WzpxaqAtYqs/maxresdefault.jpg",
+ "channel_id": "UCEFIhOEN2vXu2WzlBzrwEoA"
+ }
+ },
+ {
+ "file": "OVjH0vVVL50.wav",
+ "details": {
+ "title": "the CURE",
+ "artist": "Hannah Cottrell",
+ "video_id": "OVjH0vVVL50",
+ "duration": "3:01",
+ "thumbnail": "https://i.ytimg.com/vi/OVjH0vVVL50/maxresdefault.jpg",
+ "channel_id": "UC6YC9hRdlSzQL0S81vsnBRQ"
+ }
+ },
+ {
+ "file": "AltYMOp2p6I.wav",
+ "details": {
+ "title": "Radiate",
+ "artist": "Emil Rottmayer",
+ "video_id": "AltYMOp2p6I",
+ "duration": "3:52",
+ "thumbnail": "https://i.ytimg.com/vi/AltYMOp2p6I/maxresdefault.jpg",
+ "channel_id": "UCHagumuO_g8dwLmYqNxlTKA"
+ }
+ },
+ {
+ "file": "Oam9WmzxEFU.wav",
+ "details": {
+ "title": "Sonido Cósmico",
+ "artist": "Hermanos Gutiérrez",
+ "video_id": "Oam9WmzxEFU",
+ "duration": "3:44",
+ "thumbnail": "https://i.ytimg.com/vi/Oam9WmzxEFU/maxresdefault.jpg",
+ "channel_id": "UC80iwcArxltADbfXSS9oGCQ"
+ }
+ },
+ {
+ "file": "KS1k45uVIQs.wav",
+ "details": {
+ "title": "Changes",
+ "artist": "Nils Frahm",
+ "video_id": "KS1k45uVIQs",
+ "duration": "4:16",
+ "thumbnail": "https://i.ytimg.com/vi/KS1k45uVIQs/maxresdefault.jpg",
+ "channel_id": "UCHUlZT-VoVWIID4xcJZ5s6g"
+ }
+ }
+ ]
+}
playlist/2024-03/playlist.json
@@ -0,0 +1,127 @@
+{
+ "id": "PL1q1SH2wELHCiNaMJPdQG_u8H1fewTa8j",
+ "title": "singles - 2024-03",
+ "playlist": [
+ {
+ "file": "SdcPfYAlwCY.wav",
+ "details": {
+ "title": "Wicked Game",
+ "artist": "Tenacious D",
+ "video_id": "SdcPfYAlwCY",
+ "duration": "1:50",
+ "thumbnail": "https://i.ytimg.com/vi/SdcPfYAlwCY/maxresdefault.jpg",
+ "channel_id": "UChThjXuCoIJlougReIFCV1Q"
+ }
+ },
+ {
+ "file": "KqrQxpyXaWo.wav",
+ "details": {
+ "title": "Driver's Seat",
+ "artist": "Madds Buckley",
+ "video_id": "KqrQxpyXaWo",
+ "duration": "3:31",
+ "thumbnail": "https://i.ytimg.com/vi/KqrQxpyXaWo/maxresdefault.jpg",
+ "channel_id": "UCjsEmXojnSBRpGM7S2X7Xww"
+ }
+ },
+ {
+ "file": "hEAMhsRE5rc.wav",
+ "details": {
+ "title": "Cheerleader",
+ "artist": "Porter Robinson",
+ "video_id": "hEAMhsRE5rc",
+ "duration": "3:58",
+ "thumbnail": "https://i.ytimg.com/vi/hEAMhsRE5rc/maxresdefault.jpg",
+ "channel_id": "UCUt2uP6O_UBJp4aBx5KjQjA"
+ }
+ },
+ {
+ "file": "Kzi6aHG0-r4.wav",
+ "details": {
+ "title": "In The End",
+ "artist": "Dabin",
+ "video_id": "Kzi6aHG0-r4",
+ "duration": "3:25",
+ "thumbnail": "https://i.ytimg.com/vi/Kzi6aHG0-r4/maxresdefault.jpg",
+ "channel_id": "UCQ6yypykkyPLM5FVhOm4Eog"
+ }
+ },
+ {
+ "file": "msugPlJJ19g.wav",
+ "details": {
+ "title": "Right Back to It",
+ "artist": "Waxahatchee",
+ "video_id": "msugPlJJ19g",
+ "duration": "4:34",
+ "thumbnail": "https://i.ytimg.com/vi/msugPlJJ19g/maxresdefault.jpg",
+ "channel_id": "UCcOvXhB6k11aVrsT05oeqqg"
+ }
+ },
+ {
+ "file": "NohQpfbBZlY.wav",
+ "details": {
+ "title": "Take Me to the River",
+ "artist": "Lorde",
+ "video_id": "NohQpfbBZlY",
+ "duration": "4:25",
+ "thumbnail": "https://i.ytimg.com/vi/NohQpfbBZlY/maxresdefault.jpg",
+ "channel_id": "UCILoG0-vdgLDnrwtRuM_0GQ"
+ }
+ },
+ {
+ "file": "_qPXv-yjPv8.wav",
+ "details": {
+ "title": "Overthinker",
+ "artist": "INZO",
+ "video_id": "_qPXv-yjPv8",
+ "duration": "4:29",
+ "thumbnail": "https://i.ytimg.com/vi/_qPXv-yjPv8/maxresdefault.jpg",
+ "channel_id": "UCxI-cY6FoRygxIzElZnSchg"
+ }
+ },
+ {
+ "file": "5w69MaQVkyk.wav",
+ "details": {
+ "title": "deal with it (Enkei Remix)",
+ "artist": "Frou Frou",
+ "video_id": "5w69MaQVkyk",
+ "duration": "3:43",
+ "thumbnail": "https://i.ytimg.com/vi/5w69MaQVkyk/maxresdefault.jpg",
+ "channel_id": "UCdg2di8XrYG2cDSL_jr9kwg"
+ }
+ },
+ {
+ "file": "3TjZKqfcR9M.wav",
+ "details": {
+ "title": "Comatose",
+ "artist": "Low Hum",
+ "video_id": "3TjZKqfcR9M",
+ "duration": "6:17",
+ "thumbnail": "https://i.ytimg.com/vi/3TjZKqfcR9M/maxresdefault.jpg",
+ "channel_id": "UCCNfuP-h8YstlzfM3iR9w5w"
+ }
+ },
+ {
+ "file": "dzaLYSlYpsY.wav",
+ "details": {
+ "title": "Nocturnal",
+ "artist": "Laffey",
+ "video_id": "dzaLYSlYpsY",
+ "duration": "2:20",
+ "thumbnail": "https://i.ytimg.com/vi/dzaLYSlYpsY/maxresdefault.jpg",
+ "channel_id": "UCAHOIrzqnVqrkQAjgZlkyPw"
+ }
+ },
+ {
+ "file": "-ohBjxx2fXg.wav",
+ "details": {
+ "title": "Moon",
+ "artist": "Wice",
+ "video_id": "-ohBjxx2fXg",
+ "duration": "3:46",
+ "thumbnail": "https://i.ytimg.com/vi/-ohBjxx2fXg/maxresdefault.jpg",
+ "channel_id": "UCi7kz-1PuiH_aLbIXGNjUoQ"
+ }
+ }
+ ]
+}
playlist/2024-04/playlist.json
@@ -0,0 +1,116 @@
+{
+ "id": "PL1q1SH2wELHBVS5_Wksu6Ekq2vsZ2MWQZ",
+ "title": "singles - 2024-04",
+ "playlist": [
+ {
+ "file": "YpX0Hfpnhbw.wav",
+ "details": {
+ "title": "Some Type Of Skin",
+ "artist": "AURORA",
+ "video_id": "YpX0Hfpnhbw",
+ "duration": "3:13",
+ "thumbnail": "https://i.ytimg.com/vi/YpX0Hfpnhbw/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "k40roqQn5Lw.wav",
+ "details": {
+ "title": "Knock Yourself Out XD",
+ "artist": "Porter Robinson",
+ "video_id": "k40roqQn5Lw",
+ "duration": "2:49",
+ "thumbnail": "https://i.ytimg.com/vi/k40roqQn5Lw/maxresdefault.jpg",
+ "channel_id": "UCUt2uP6O_UBJp4aBx5KjQjA"
+ }
+ },
+ {
+ "file": "ujWjuvCadaM.wav",
+ "details": {
+ "title": "Crush",
+ "artist": "Tourist",
+ "video_id": "ujWjuvCadaM",
+ "duration": "5:00",
+ "thumbnail": "https://i.ytimg.com/vi/ujWjuvCadaM/maxresdefault.jpg",
+ "channel_id": "UCP-mlMKCt28XuYwWvECgtAw"
+ }
+ },
+ {
+ "file": "LBJB-OSlbD4.wav",
+ "details": {
+ "title": "My Fun",
+ "artist": "Suki Waterhouse",
+ "video_id": "LBJB-OSlbD4",
+ "duration": "2:43",
+ "thumbnail": "https://i.ytimg.com/vi/LBJB-OSlbD4/maxresdefault.jpg",
+ "channel_id": "UCeRJ4IHCfysUxLq1fRnXCeQ"
+ }
+ },
+ {
+ "file": "30X8L7h1Pno.wav",
+ "details": {
+ "title": "So Long Ago",
+ "artist": "Kowloon",
+ "video_id": "30X8L7h1Pno",
+ "duration": "3:24",
+ "thumbnail": "https://i.ytimg.com/vi/30X8L7h1Pno/maxresdefault.jpg",
+ "channel_id": "UCsYv_tQdvyI6AtnK_uHI7-g"
+ }
+ },
+ {
+ "file": "ZQkubQjqNCc.wav",
+ "details": {
+ "title": "Deeper Well",
+ "artist": "Kacey Musgraves",
+ "video_id": "ZQkubQjqNCc",
+ "duration": "3:53",
+ "thumbnail": "https://i.ytimg.com/vi/ZQkubQjqNCc/maxresdefault.jpg",
+ "channel_id": "UC87QNTF9qgTwe8KKN1V8NGw"
+ }
+ },
+ {
+ "file": "IAoPryBArwc.wav",
+ "details": {
+ "title": "All Falls Down",
+ "artist": "Lizzy McAlpine",
+ "video_id": "IAoPryBArwc",
+ "duration": "2:52",
+ "thumbnail": "https://i.ytimg.com/vi/IAoPryBArwc/maxresdefault.jpg",
+ "channel_id": "UCC1TOSYNYXw1xrwNRxW46dg"
+ }
+ },
+ {
+ "file": "pk2Sr1ClJbQ.wav",
+ "details": {
+ "title": "iwaly",
+ "artist": "Shallou",
+ "video_id": "pk2Sr1ClJbQ",
+ "duration": "3:22",
+ "thumbnail": "https://i.ytimg.com/vi/pk2Sr1ClJbQ/maxresdefault.jpg",
+ "channel_id": "UCx-EbB855hgbn4t-ZWDEMWQ"
+ }
+ },
+ {
+ "file": "91EKXfenlCQ.wav",
+ "details": {
+ "title": "Carousel",
+ "artist": "Ghostly Kisses",
+ "video_id": "91EKXfenlCQ",
+ "duration": "3:56",
+ "thumbnail": "https://i.ytimg.com/vi/91EKXfenlCQ/maxresdefault.jpg",
+ "channel_id": "UCZdcdkgw9Jnt8ZlfUXUyyFw"
+ }
+ },
+ {
+ "file": "p1Li84lDQ6g.wav",
+ "details": {
+ "title": "Until We Meet Again",
+ "artist": "Hermanos Gutiérrez",
+ "video_id": "p1Li84lDQ6g",
+ "duration": "3:02",
+ "thumbnail": "https://i.ytimg.com/vi/p1Li84lDQ6g/maxresdefault.jpg",
+ "channel_id": "UC80iwcArxltADbfXSS9oGCQ"
+ }
+ }
+ ]
+}
playlist/2024-05/playlist.json
@@ -0,0 +1,259 @@
+{
+ "id": "PL1q1SH2wELHD3layJxEWUUDgt6IDEy250",
+ "title": "singles - 2024-05",
+ "playlist": [
+ {
+ "file": "AYyk6qsjmjU.wav",
+ "details": {
+ "title": "Bloom",
+ "artist": "Swim Surreal",
+ "video_id": "AYyk6qsjmjU",
+ "duration": "3:35",
+ "thumbnail": "https://i.ytimg.com/vi/AYyk6qsjmjU/maxresdefault.jpg",
+ "channel_id": "UCAZFBEQ9Tjqnd2xXkMq45iQ"
+ }
+ },
+ {
+ "file": "KjfRmEs4iqU.wav",
+ "details": {
+ "title": "Some Type Of Skin (Acoustic)",
+ "artist": "AURORA",
+ "video_id": "KjfRmEs4iqU",
+ "duration": "3:20",
+ "thumbnail": "https://i.ytimg.com/vi/KjfRmEs4iqU/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "8a9o16X01H0.wav",
+ "details": {
+ "title": "Do You Ever Wonder? (Stripped)",
+ "artist": "Meltt",
+ "video_id": "8a9o16X01H0",
+ "duration": "3:50",
+ "thumbnail": "https://i.ytimg.com/vi/8a9o16X01H0/maxresdefault.jpg",
+ "channel_id": "UCQjoG_DwO-JwwCMXtJBX83g"
+ }
+ },
+ {
+ "file": "Rsecp8tPgMw.wav",
+ "details": {
+ "title": "At Your Feet",
+ "artist": "Bat For Lashes",
+ "video_id": "Rsecp8tPgMw",
+ "duration": "3:46",
+ "thumbnail": "https://i.ytimg.com/vi/Rsecp8tPgMw/maxresdefault.jpg",
+ "channel_id": "UCy6CFW9W6aaoADjzomqk3Jg"
+ }
+ },
+ {
+ "file": "QUKykgoD604.wav",
+ "details": {
+ "title": "Traces",
+ "artist": "Louie Zong",
+ "video_id": "QUKykgoD604",
+ "duration": "2:55",
+ "thumbnail": "https://i.ytimg.com/vi/QUKykgoD604/maxresdefault.jpg",
+ "channel_id": "UC8yZNEyLobqwhMc5fFYLYuw"
+ }
+ },
+ {
+ "file": "EoSR3RVy2Ag.wav",
+ "details": {
+ "title": "Epocha",
+ "artist": "Kodomo",
+ "video_id": "EoSR3RVy2Ag",
+ "duration": "4:16",
+ "thumbnail": "https://i.ytimg.com/vi/EoSR3RVy2Ag/maxresdefault.jpg",
+ "channel_id": "UCBZUfcsTXduNosmS4eACRYg"
+ }
+ },
+ {
+ "file": "SLe2w-A2UHw.wav",
+ "details": {
+ "title": "Lost Signals",
+ "artist": "Kodomo",
+ "video_id": "SLe2w-A2UHw",
+ "duration": "7:30",
+ "thumbnail": "https://i.ytimg.com/vi/SLe2w-A2UHw/maxresdefault.jpg",
+ "channel_id": "UCBZUfcsTXduNosmS4eACRYg"
+ }
+ },
+ {
+ "file": "fZfuH7hlIXM.wav",
+ "details": {
+ "title": "Remnants",
+ "artist": "Kodomo",
+ "video_id": "fZfuH7hlIXM",
+ "duration": "5:33",
+ "thumbnail": "https://i.ytimg.com/vi/fZfuH7hlIXM/maxresdefault.jpg",
+ "channel_id": "UCBZUfcsTXduNosmS4eACRYg"
+ }
+ },
+ {
+ "file": "XIHzv0rKh68.wav",
+ "details": {
+ "title": "Traces Of Beauty",
+ "artist": "Kodomo",
+ "video_id": "XIHzv0rKh68",
+ "duration": "2:36",
+ "thumbnail": "https://i.ytimg.com/vi/XIHzv0rKh68/maxresdefault.jpg",
+ "channel_id": "UCBZUfcsTXduNosmS4eACRYg"
+ }
+ },
+ {
+ "file": "DaBJ_uIBLfg.wav",
+ "details": {
+ "title": "Alpha Omega",
+ "artist": "Kodomo",
+ "video_id": "DaBJ_uIBLfg",
+ "duration": "5:11",
+ "thumbnail": "https://i.ytimg.com/vi/DaBJ_uIBLfg/maxresdefault.jpg",
+ "channel_id": "UCBZUfcsTXduNosmS4eACRYg"
+ }
+ },
+ {
+ "file": "RLcm7-2Xm3g.wav",
+ "details": {
+ "title": "Animal Soul",
+ "artist": "AURORA",
+ "video_id": "RLcm7-2Xm3g",
+ "duration": "3:03",
+ "thumbnail": "https://i.ytimg.com/vi/RLcm7-2Xm3g/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "0ymCdRe2y0c.wav",
+ "details": {
+ "title": "Barrio Hustle",
+ "artist": "Hermanos Gutiérrez",
+ "video_id": "0ymCdRe2y0c",
+ "duration": "3:15",
+ "thumbnail": "https://i.ytimg.com/vi/0ymCdRe2y0c/maxresdefault.jpg",
+ "channel_id": "UC80iwcArxltADbfXSS9oGCQ"
+ }
+ },
+ {
+ "file": "GsVd7GEH_Tc.wav",
+ "details": {
+ "title": "Girls (Live)",
+ "artist": "Slow Magic",
+ "video_id": "GsVd7GEH_Tc",
+ "duration": "4:50",
+ "thumbnail": "https://i.ytimg.com/vi/GsVd7GEH_Tc/maxresdefault.jpg",
+ "channel_id": "UCmUORL97OeG5TxWhVAsB40Q"
+ }
+ },
+ {
+ "file": "7L2arNJcANY.wav",
+ "details": {
+ "title": "Evening In The City",
+ "artist": "Laffey",
+ "video_id": "7L2arNJcANY",
+ "duration": "2:44",
+ "thumbnail": "https://i.ytimg.com/vi/7L2arNJcANY/maxresdefault.jpg",
+ "channel_id": "UCAHOIrzqnVqrkQAjgZlkyPw"
+ }
+ },
+ {
+ "file": "Z_YoF_Dp7k0.wav",
+ "details": {
+ "title": "Thrown Around",
+ "artist": "James Blake",
+ "video_id": "Z_YoF_Dp7k0",
+ "duration": "4:21",
+ "thumbnail": "https://i.ytimg.com/vi/Z_YoF_Dp7k0/maxresdefault.jpg",
+ "channel_id": "UCx90L12QXzc18AGYFRof-EA"
+ }
+ },
+ {
+ "file": "YAMd-a1M55s.wav",
+ "details": {
+ "title": "The Cosmos",
+ "artist": "Sylvan Esso",
+ "video_id": "YAMd-a1M55s",
+ "duration": "3:42",
+ "thumbnail": "https://i.ytimg.com/vi/YAMd-a1M55s/maxresdefault.jpg",
+ "channel_id": "UCo2TtupcsAkNLwwK6yrO9dw"
+ }
+ },
+ {
+ "file": "duzshvkv4ic.wav",
+ "details": {
+ "title": "Hey Mami (Rick Wade Remix)",
+ "artist": "Sylvan Esso",
+ "video_id": "duzshvkv4ic",
+ "duration": "6:59",
+ "thumbnail": "https://i.ytimg.com/vi/duzshvkv4ic/maxresdefault.jpg",
+ "channel_id": "UCo2TtupcsAkNLwwK6yrO9dw"
+ }
+ },
+ {
+ "file": "1wXal5cF0Ok.wav",
+ "details": {
+ "title": "Arc on the Range",
+ "artist": "Amtrac",
+ "video_id": "1wXal5cF0Ok",
+ "duration": "4:40",
+ "thumbnail": "https://i.ytimg.com/vi/1wXal5cF0Ok/maxresdefault.jpg",
+ "channel_id": "UCKQTvdyEWFnxYlmKPAdtmLA"
+ }
+ },
+ {
+ "file": "dwZB3Fx3MSg.wav",
+ "details": {
+ "title": "Sexy to Someone",
+ "artist": "Clairo",
+ "video_id": "dwZB3Fx3MSg",
+ "duration": "3:28",
+ "thumbnail": "https://i.ytimg.com/vi/dwZB3Fx3MSg/maxresdefault.jpg",
+ "channel_id": "UChC2OO1dG5Vge1Kuc-1QZRg"
+ }
+ },
+ {
+ "file": "Ttz9_2Cr-gM.wav",
+ "details": {
+ "title": "Kind Of Man",
+ "artist": "London Grammar",
+ "video_id": "Ttz9_2Cr-gM",
+ "duration": "4:16",
+ "thumbnail": "https://i.ytimg.com/vi/Ttz9_2Cr-gM/maxresdefault.jpg",
+ "channel_id": "UCOkff6wNpN2LaEcuNe0bf8w"
+ }
+ },
+ {
+ "file": "j-FM9WNq610.wav",
+ "details": {
+ "title": "I Cover the Waterfront",
+ "artist": "Andrew Bird",
+ "video_id": "j-FM9WNq610",
+ "duration": "4:52",
+ "thumbnail": "https://i.ytimg.com/vi/j-FM9WNq610/maxresdefault.jpg",
+ "channel_id": "UCWWyLppgObl9xS8wdSpiv8Q"
+ }
+ },
+ {
+ "file": "AsV4dYIXyrw.wav",
+ "details": {
+ "title": "I Fall in Love Too Easily",
+ "artist": "Andrew Bird",
+ "video_id": "AsV4dYIXyrw",
+ "duration": "4:00",
+ "thumbnail": "https://i.ytimg.com/vi/AsV4dYIXyrw/maxresdefault.jpg",
+ "channel_id": "UCWWyLppgObl9xS8wdSpiv8Q"
+ }
+ },
+ {
+ "file": "C_9dQRNOxNM.wav",
+ "details": {
+ "title": "I’ve Grown Accustomed to Her Face",
+ "artist": "Andrew Bird",
+ "video_id": "C_9dQRNOxNM",
+ "duration": "1:50",
+ "thumbnail": "https://i.ytimg.com/vi/C_9dQRNOxNM/maxresdefault.jpg",
+ "channel_id": "UCWWyLppgObl9xS8wdSpiv8Q"
+ }
+ }
+ ]
+}
playlist/2024-08/playlist.json
@@ -0,0 +1,182 @@
+{
+ "id": "PL1q1SH2wELHC1T-QipIG95HfluEp5Ti_R",
+ "title": "singles - 2024-08",
+ "playlist": [
+ {
+ "file": "f1XfGopQyK4.wav",
+ "details": {
+ "title": "Sitting Beachside (Remix)",
+ "artist": "Six Missing",
+ "video_id": "f1XfGopQyK4",
+ "duration": "5:14",
+ "thumbnail": "https://i.ytimg.com/vi/f1XfGopQyK4/maxresdefault.jpg",
+ "channel_id": "UCQOKYHM3bAJ0hIYyhdWKtjg"
+ }
+ },
+ {
+ "file": "1TUzPMR8GtI.wav",
+ "details": {
+ "title": "Running Out Of Time",
+ "artist": "Ashe",
+ "video_id": "1TUzPMR8GtI",
+ "duration": "3:35",
+ "thumbnail": "https://i.ytimg.com/vi/1TUzPMR8GtI/maxresdefault.jpg",
+ "channel_id": "UCu4DT36ncDBhuBo1w_vZsFg"
+ }
+ },
+ {
+ "file": "aRkPKeZFkuw.wav",
+ "details": {
+ "title": "For Cryin' Out Loud!",
+ "artist": "FINNEAS",
+ "video_id": "aRkPKeZFkuw",
+ "duration": "3:38",
+ "thumbnail": "https://i.ytimg.com/vi/aRkPKeZFkuw/maxresdefault.jpg",
+ "channel_id": "UCrbZcTv3iAHqMHWGbk0IYFA"
+ }
+ },
+ {
+ "file": "SqJXMLqu64Q.wav",
+ "details": {
+ "title": "Never Meant",
+ "artist": "Iron \u0026 Wine",
+ "video_id": "SqJXMLqu64Q",
+ "duration": "3:20",
+ "thumbnail": "https://i.ytimg.com/vi/SqJXMLqu64Q/maxresdefault.jpg",
+ "channel_id": "UCbPM7GBZG0U7Fz5FsnoUIag"
+ }
+ },
+ {
+ "file": "CJMKIUWj5BI.wav",
+ "details": {
+ "title": "M",
+ "artist": "Soccer Mommy",
+ "video_id": "CJMKIUWj5BI",
+ "duration": "3:52",
+ "thumbnail": "https://i.ytimg.com/vi/CJMKIUWj5BI/maxresdefault.jpg",
+ "channel_id": "UCiUjSTW94mp2xfW9jqotQlg"
+ }
+ },
+ {
+ "file": "n2JFZdah9YU.wav",
+ "details": {
+ "title": "Rainbow Overpass",
+ "artist": "Bright Eyes",
+ "video_id": "n2JFZdah9YU",
+ "duration": "3:02",
+ "thumbnail": "https://i.ytimg.com/vi/n2JFZdah9YU/maxresdefault.jpg",
+ "channel_id": "UCPI3znNhox-PTRPvpYFNCDw"
+ }
+ },
+ {
+ "file": "-KMzHTIuzr0.wav",
+ "details": {
+ "title": "Oasis",
+ "artist": "Softy",
+ "video_id": "-KMzHTIuzr0",
+ "duration": "1:50",
+ "thumbnail": "https://i.ytimg.com/vi/-KMzHTIuzr0/maxresdefault.jpg",
+ "channel_id": "UCtjbjXoaBzpZhprRKI-7r7A"
+ }
+ },
+ {
+ "file": "pHDDDcSI0Uw.wav",
+ "details": {
+ "title": "Gentle Stream",
+ "artist": "Goson",
+ "video_id": "pHDDDcSI0Uw",
+ "duration": "2:00",
+ "thumbnail": "https://i.ytimg.com/vi/pHDDDcSI0Uw/maxresdefault.jpg",
+ "channel_id": "UC2IORNr_9-KmsVTvSaaEOGw"
+ }
+ },
+ {
+ "file": "3qBN8hO_t68.wav",
+ "details": {
+ "title": "Simpler Times",
+ "artist": "Laffey",
+ "video_id": "3qBN8hO_t68",
+ "duration": "2:17",
+ "thumbnail": "https://i.ytimg.com/vi/3qBN8hO_t68/maxresdefault.jpg",
+ "channel_id": "UCAHOIrzqnVqrkQAjgZlkyPw"
+ }
+ },
+ {
+ "file": "lq7N4SjzUQU.wav",
+ "details": {
+ "title": "Romantic Rights",
+ "artist": "Low Hum",
+ "video_id": "lq7N4SjzUQU",
+ "duration": "3:11",
+ "thumbnail": "https://i.ytimg.com/vi/lq7N4SjzUQU/maxresdefault.jpg",
+ "channel_id": "UCCNfuP-h8YstlzfM3iR9w5w"
+ }
+ },
+ {
+ "file": "WaSr1ureGOw.wav",
+ "details": {
+ "title": "Is",
+ "artist": "Hiatus",
+ "video_id": "WaSr1ureGOw",
+ "duration": "4:21",
+ "thumbnail": "https://i.ytimg.com/vi/WaSr1ureGOw/maxresdefault.jpg",
+ "channel_id": "UCwTt8gBv6lRGMaShDbmwaqQ"
+ }
+ },
+ {
+ "file": "DcoGLo-3YBE.wav",
+ "details": {
+ "title": "Green",
+ "artist": "Tycho",
+ "video_id": "DcoGLo-3YBE",
+ "duration": "4:29",
+ "thumbnail": "https://i.ytimg.com/vi/DcoGLo-3YBE/maxresdefault.jpg",
+ "channel_id": "UC6M6DgivBo3VHTYDa9BblqQ"
+ }
+ },
+ {
+ "file": "9Uk5ur2HOJU.wav",
+ "details": {
+ "title": "Flower With No Name",
+ "artist": "Narvent",
+ "video_id": "9Uk5ur2HOJU",
+ "duration": "2:52",
+ "thumbnail": "https://i.ytimg.com/vi/9Uk5ur2HOJU/maxresdefault.jpg",
+ "channel_id": "UCf0MXejn3VTMr1tvPFszwOg"
+ }
+ },
+ {
+ "file": "tzIjD6YqYkk.wav",
+ "details": {
+ "title": "Fainted",
+ "artist": "Narvent",
+ "video_id": "tzIjD6YqYkk",
+ "duration": "4:10",
+ "thumbnail": "https://i.ytimg.com/vi/tzIjD6YqYkk/maxresdefault.jpg",
+ "channel_id": "UCf0MXejn3VTMr1tvPFszwOg"
+ }
+ },
+ {
+ "file": "vCRx_eQh370.wav",
+ "details": {
+ "title": "With You",
+ "artist": "Sung",
+ "video_id": "vCRx_eQh370",
+ "duration": "4:48",
+ "thumbnail": "https://i.ytimg.com/vi/vCRx_eQh370/maxresdefault.jpg",
+ "channel_id": "UCpDhdxcTogJ0XpHhOBcKDHw"
+ }
+ },
+ {
+ "file": "3C4KVaLIGK8.wav",
+ "details": {
+ "title": "Virtual Machine",
+ "artist": "Navjaxx",
+ "video_id": "3C4KVaLIGK8",
+ "duration": "3:04",
+ "thumbnail": "https://i.ytimg.com/vi/3C4KVaLIGK8/maxresdefault.jpg",
+ "channel_id": "UCXuxp8ykEgi3yDFLPPIWCCw"
+ }
+ }
+ ]
+}
playlist/2025-02/playlist.json
@@ -0,0 +1,138 @@
+{
+ "id": "PL1q1SH2wELHAyugNnMfEb9Of-mUhbJeub",
+ "title": "singles - 2025-02",
+ "playlist": [
+ {
+ "file": "IbT2i5RwsNY.wav",
+ "details": {
+ "title": "Tunnel Vision",
+ "artist": "Beach Bunny",
+ "video_id": "IbT2i5RwsNY",
+ "duration": "2:44",
+ "thumbnail": "https://i.ytimg.com/vi/IbT2i5RwsNY/maxresdefault.jpg",
+ "channel_id": "UCu1ZE4E9eAAWbVxvQ-JAFsg"
+ }
+ },
+ {
+ "file": "tNk_KoXP3DM.wav",
+ "details": {
+ "title": "Some Type Of Skin (feat. ATARASHII GAKKO!)",
+ "artist": "AURORA",
+ "video_id": "tNk_KoXP3DM",
+ "duration": "3:37",
+ "thumbnail": "https://i.ytimg.com/vi/tNk_KoXP3DM/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "NvuzLJ1qBiI.wav",
+ "details": {
+ "title": "In Love With A Memory",
+ "artist": "Sasami",
+ "video_id": "NvuzLJ1qBiI",
+ "duration": "4:04",
+ "thumbnail": "https://i.ytimg.com/vi/NvuzLJ1qBiI/maxresdefault.jpg",
+ "channel_id": "UCqNK1emsE-JsREsVZ1rTYmQ"
+ }
+ },
+ {
+ "file": "0Z59egLWqpY.wav",
+ "details": {
+ "title": "THINGS BEHIND THINGS BEHIND THINGS",
+ "artist": "Bon Iver",
+ "video_id": "0Z59egLWqpY",
+ "duration": "3:21",
+ "thumbnail": "https://i.ytimg.com/vi/0Z59egLWqpY/maxresdefault.jpg",
+ "channel_id": "UCx-cwf1tCXoZr0KtnpaDwyA"
+ }
+ },
+ {
+ "file": "rR6Pijjiluw.wav",
+ "details": {
+ "title": "Driver (stripped)",
+ "artist": "Soccer Mommy",
+ "video_id": "rR6Pijjiluw",
+ "duration": "4:32",
+ "thumbnail": "https://i.ytimg.com/vi/rR6Pijjiluw/maxresdefault.jpg",
+ "channel_id": "UCiUjSTW94mp2xfW9jqotQlg"
+ }
+ },
+ {
+ "file": "1SO0oVWw7Xc.wav",
+ "details": {
+ "title": "nutshell",
+ "artist": "Carlie Hanson",
+ "video_id": "1SO0oVWw7Xc",
+ "duration": "4:09",
+ "thumbnail": "https://i.ytimg.com/vi/1SO0oVWw7Xc/maxresdefault.jpg",
+ "channel_id": "UCkS39sZY2k8AUg_2boNWipg"
+ }
+ },
+ {
+ "file": "0hm03E3NQlU.wav",
+ "details": {
+ "title": "Mud",
+ "artist": "Waxahatchee",
+ "video_id": "0hm03E3NQlU",
+ "duration": "2:08",
+ "thumbnail": "https://i.ytimg.com/vi/0hm03E3NQlU/maxresdefault.jpg",
+ "channel_id": "UCcOvXhB6k11aVrsT05oeqqg"
+ }
+ },
+ {
+ "file": "xNpiTYWZ2X8.wav",
+ "details": {
+ "title": "Wire walks",
+ "artist": "Amy Millan",
+ "video_id": "xNpiTYWZ2X8",
+ "duration": "4:23",
+ "thumbnail": "https://i.ytimg.com/vi/xNpiTYWZ2X8/maxresdefault.jpg",
+ "channel_id": "UC72H3QyglTAXVlf-4P1rpXQ"
+ }
+ },
+ {
+ "file": "TzQee5ZhldE.wav",
+ "details": {
+ "title": "Orlando in Love",
+ "artist": "Japanese Breakfast",
+ "video_id": "TzQee5ZhldE",
+ "duration": "2:26",
+ "thumbnail": "https://i.ytimg.com/vi/TzQee5ZhldE/maxresdefault.jpg",
+ "channel_id": "UCYgfTCJgqA23D29kxRIRgLQ"
+ }
+ },
+ {
+ "file": "vS106x9En58.wav",
+ "details": {
+ "title": "Break For Lovers (live 2025)",
+ "artist": "Men I Trust",
+ "video_id": "vS106x9En58",
+ "duration": "3:22",
+ "thumbnail": "https://i.ytimg.com/vi/vS106x9En58/maxresdefault.jpg",
+ "channel_id": "UCaPOa_Tg0TThuYSVtUEXb8g"
+ }
+ },
+ {
+ "file": "EoTf9Ne1MX4.wav",
+ "details": {
+ "title": "Valentine (Extended)",
+ "artist": "Flower Face",
+ "video_id": "EoTf9Ne1MX4",
+ "duration": "5:00",
+ "thumbnail": "https://i.ytimg.com/vi/EoTf9Ne1MX4/maxresdefault.jpg",
+ "channel_id": "UCuwiQ5sWqdYPxtHPqpQX_nw"
+ }
+ },
+ {
+ "file": "BRnE8flpxxk.wav",
+ "details": {
+ "title": "We Didn’t Know We Were Ready",
+ "artist": "Ólafur Arnalds",
+ "video_id": "BRnE8flpxxk",
+ "duration": "6:27",
+ "thumbnail": "https://i.ytimg.com/vi/BRnE8flpxxk/maxresdefault.jpg",
+ "channel_id": "UCDQlvsMEM5j0k5cqmfHBzVA"
+ }
+ }
+ ]
+}
playlist/2025-03/playlist.json
@@ -0,0 +1,116 @@
+{
+ "id": "PL1q1SH2wELHDG5YQWaxC-evRTgXSE4-vs",
+ "title": "singles - 2025-03",
+ "playlist": [
+ {
+ "file": "e4m27fTh51M.wav",
+ "details": {
+ "title": "Big Pink Bubble",
+ "artist": "Beach Bunny",
+ "video_id": "e4m27fTh51M",
+ "duration": "1:58",
+ "thumbnail": "https://i.ytimg.com/vi/e4m27fTh51M/maxresdefault.jpg",
+ "channel_id": "UCu1ZE4E9eAAWbVxvQ-JAFsg"
+ }
+ },
+ {
+ "file": "LEitvT0-mhc.wav",
+ "details": {
+ "title": "Nothing I Need",
+ "artist": "Lord Huron",
+ "video_id": "LEitvT0-mhc",
+ "duration": "3:34",
+ "thumbnail": "https://i.ytimg.com/vi/LEitvT0-mhc/maxresdefault.jpg",
+ "channel_id": "UCHIqmk90GKurdVSG5cf-sFQ"
+ }
+ },
+ {
+ "file": "u2tuMTQfOY8.wav",
+ "details": {
+ "title": "Part of the Dream",
+ "artist": "Vansire",
+ "video_id": "u2tuMTQfOY8",
+ "duration": "2:20",
+ "thumbnail": "https://i.ytimg.com/vi/u2tuMTQfOY8/maxresdefault.jpg",
+ "channel_id": "UCXnU9Wc2jnOuSo1jzFhAA9g"
+ }
+ },
+ {
+ "file": "ExIpnf9y95w.wav",
+ "details": {
+ "title": "Ankles",
+ "artist": "Lucy Dacus",
+ "video_id": "ExIpnf9y95w",
+ "duration": "3:12",
+ "thumbnail": "https://i.ytimg.com/vi/ExIpnf9y95w/maxresdefault.jpg",
+ "channel_id": "UCt-KqR_ceCdjeMljGYiXSww"
+ }
+ },
+ {
+ "file": "6hYza8QW0fY.wav",
+ "details": {
+ "title": "Life On The Line",
+ "artist": "The Walters",
+ "video_id": "6hYza8QW0fY",
+ "duration": "2:08",
+ "thumbnail": "https://i.ytimg.com/vi/6hYza8QW0fY/maxresdefault.jpg",
+ "channel_id": "UCWXnGZRSAZJQ7JV7id5fufg"
+ }
+ },
+ {
+ "file": "GiugMci-Cd8.wav",
+ "details": {
+ "title": "million times",
+ "artist": "Esha Tewari",
+ "video_id": "GiugMci-Cd8",
+ "duration": "3:03",
+ "thumbnail": "https://i.ytimg.com/vi/GiugMci-Cd8/maxresdefault.jpg",
+ "channel_id": "UCMwpTCmVW4xaf6wIgnmfRYA"
+ }
+ },
+ {
+ "file": "MtCsMBXuZ84.wav",
+ "details": {
+ "title": "Orlando in Love",
+ "artist": "Japanese Breakfast",
+ "video_id": "MtCsMBXuZ84",
+ "duration": "2:26",
+ "thumbnail": "https://i.ytimg.com/vi/MtCsMBXuZ84/maxresdefault.jpg",
+ "channel_id": "UCYgfTCJgqA23D29kxRIRgLQ"
+ }
+ },
+ {
+ "file": "UBL1PqqD6ic.wav",
+ "details": {
+ "title": "Do I Wanna Know?",
+ "artist": "Willowbay",
+ "video_id": "UBL1PqqD6ic",
+ "duration": "3:40",
+ "thumbnail": "https://i.ytimg.com/vi/UBL1PqqD6ic/maxresdefault.jpg",
+ "channel_id": "UCLGR57dIsJi7mxtk_1iha8A"
+ }
+ },
+ {
+ "file": "mpW-TaY3hQw.wav",
+ "details": {
+ "title": "Runaway (Orchestral)",
+ "artist": "AURORA",
+ "video_id": "mpW-TaY3hQw",
+ "duration": "4:09",
+ "thumbnail": "https://i.ytimg.com/vi/mpW-TaY3hQw/maxresdefault.jpg",
+ "channel_id": "UC4G-AJa7kn8oumI6TT2WXYw"
+ }
+ },
+ {
+ "file": "MmPKV_6-2EQ.wav",
+ "details": {
+ "title": "The Landkeeper",
+ "artist": "Men I Trust",
+ "video_id": "MmPKV_6-2EQ",
+ "duration": "3:45",
+ "thumbnail": "https://i.ytimg.com/vi/MmPKV_6-2EQ/maxresdefault.jpg",
+ "channel_id": "UCaPOa_Tg0TThuYSVtUEXb8g"
+ }
+ }
+ ]
+}
playlist/2025-04/playlist.json
@@ -0,0 +1,149 @@
+{
+ "id": "PL1q1SH2wELHDBJtIEB5fL6cCvQCkE323S",
+ "title": "singles - 2025-04",
+ "playlist": [
+ {
+ "file": "pILVJjLJHnk.wav",
+ "details": {
+ "title": "Anthems For A Seventeen Year-Old Girl",
+ "artist": "Maggie Rogers",
+ "video_id": "pILVJjLJHnk",
+ "duration": "3:37",
+ "thumbnail": "https://i.ytimg.com/vi/pILVJjLJHnk/maxresdefault.jpg",
+ "channel_id": "UCILdfzf4yvWOk8Zmcx2mm7g"
+ }
+ },
+ {
+ "file": "SKvU-9-Rv-M.wav",
+ "details": {
+ "title": "Clueless",
+ "artist": "Beach Bunny",
+ "video_id": "SKvU-9-Rv-M",
+ "duration": "3:29",
+ "thumbnail": "https://i.ytimg.com/vi/SKvU-9-Rv-M/maxresdefault.jpg",
+ "channel_id": "UCu1ZE4E9eAAWbVxvQ-JAFsg"
+ }
+ },
+ {
+ "file": "ebyG5yrlc3s.wav",
+ "details": {
+ "title": "Down to be wrong",
+ "artist": "HAIM",
+ "video_id": "ebyG5yrlc3s",
+ "duration": "4:10",
+ "thumbnail": "https://i.ytimg.com/vi/ebyG5yrlc3s/maxresdefault.jpg",
+ "channel_id": "UCxUTLbpwMNwO6g8kx-BHdQg"
+ }
+ },
+ {
+ "file": "3muhbPCSETk.wav",
+ "details": {
+ "title": "Metal",
+ "artist": "The Beths",
+ "video_id": "3muhbPCSETk",
+ "duration": "4:44",
+ "thumbnail": "https://i.ytimg.com/vi/3muhbPCSETk/maxresdefault.jpg",
+ "channel_id": "UCLFyXW9u1q-QINz7u7rxlFg"
+ }
+ },
+ {
+ "file": "iRPnEdR5h9o.wav",
+ "details": {
+ "title": "Spring Into Summer (Live from MGM Music Hall, Boston)",
+ "artist": "Lizzy McAlpine",
+ "video_id": "iRPnEdR5h9o",
+ "duration": "4:22",
+ "thumbnail": "https://i.ytimg.com/vi/iRPnEdR5h9o/maxresdefault.jpg",
+ "channel_id": "UCC1TOSYNYXw1xrwNRxW46dg"
+ }
+ },
+ {
+ "file": "m_cbsJcjBw0.wav",
+ "details": {
+ "title": "Lake Street",
+ "artist": "W O L F C L U B",
+ "video_id": "m_cbsJcjBw0",
+ "duration": "4:41",
+ "thumbnail": "https://i.ytimg.com/vi/m_cbsJcjBw0/maxresdefault.jpg",
+ "channel_id": "UCt1XzuTdfAv8Lbx5YRovcZQ"
+ }
+ },
+ {
+ "file": "HmxMvVEIiw0.wav",
+ "details": {
+ "title": "What Was That",
+ "artist": "Lorde",
+ "video_id": "HmxMvVEIiw0",
+ "duration": "3:30",
+ "thumbnail": "https://i.ytimg.com/vi/HmxMvVEIiw0/maxresdefault.jpg",
+ "channel_id": "UCILoG0-vdgLDnrwtRuM_0GQ"
+ }
+ },
+ {
+ "file": "lsOM1lRs3r8.wav",
+ "details": {
+ "title": "Nobody New",
+ "artist": "The Marías",
+ "video_id": "lsOM1lRs3r8",
+ "duration": "3:36",
+ "thumbnail": "https://i.ytimg.com/vi/lsOM1lRs3r8/maxresdefault.jpg",
+ "channel_id": "UCVV5M4OEFsKnB9HBhwOhHbA"
+ }
+ },
+ {
+ "file": "sQzGvczLNWs.wav",
+ "details": {
+ "title": "Pink Elephant",
+ "artist": "Arcade Fire",
+ "video_id": "sQzGvczLNWs",
+ "duration": "4:45",
+ "thumbnail": "https://i.ytimg.com/vi/sQzGvczLNWs/maxresdefault.jpg",
+ "channel_id": "UCXvAK640Ko85SLbI-zA0ZCg"
+ }
+ },
+ {
+ "file": "YjI-4DOxDXI.wav",
+ "details": {
+ "title": "Bluebird",
+ "artist": "Lana Del Rey",
+ "video_id": "YjI-4DOxDXI",
+ "duration": "4:03",
+ "thumbnail": "https://i.ytimg.com/vi/YjI-4DOxDXI/maxresdefault.jpg",
+ "channel_id": "UCyqq-aiu3vEHuf5NhwmOJcw"
+ }
+ },
+ {
+ "file": "VtsncYTY3_0.wav",
+ "details": {
+ "title": "Kelly Watch the Stars (Vegyn Version)",
+ "artist": "Air",
+ "video_id": "VtsncYTY3_0",
+ "duration": "3:36",
+ "thumbnail": "https://i.ytimg.com/vi/VtsncYTY3_0/maxresdefault.jpg",
+ "channel_id": "UCH_Q3OLUXLZNF0qmnY3bCXg"
+ }
+ },
+ {
+ "file": "JbDCXgeoAyI.wav",
+ "details": {
+ "title": "Artifacts Of A Higher Dimension",
+ "artist": "INZO",
+ "video_id": "JbDCXgeoAyI",
+ "duration": "5:12",
+ "thumbnail": "https://i.ytimg.com/vi/JbDCXgeoAyI/maxresdefault.jpg",
+ "channel_id": "UCxI-cY6FoRygxIzElZnSchg"
+ }
+ },
+ {
+ "file": "Xe0MMAUPmeI.wav",
+ "details": {
+ "title": "Sounds from the Heart of the Woods",
+ "artist": "Kacey Musgraves",
+ "video_id": "Xe0MMAUPmeI",
+ "duration": "21:08",
+ "thumbnail": "https://i.ytimg.com/vi/Xe0MMAUPmeI/maxresdefault.jpg",
+ "channel_id": "UC87QNTF9qgTwe8KKN1V8NGw"
+ }
+ }
+ ]
+}
playlist/2025-05/playlist.json
@@ -0,0 +1,160 @@
+{
+ "id": "PL1q1SH2wELHDcBBOCpguM6fjOJA5A9V5d",
+ "title": "singles - 2025-05",
+ "playlist": [
+ {
+ "file": "KqP7sY3MFgU.wav",
+ "details": {
+ "title": "23's A Baby",
+ "artist": "Blondshell",
+ "video_id": "KqP7sY3MFgU",
+ "duration": "4:14",
+ "thumbnail": "https://i.ytimg.com/vi/KqP7sY3MFgU/maxresdefault.jpg",
+ "channel_id": "UC6Ziy1PmgqE6O1lFwyJp1dg"
+ }
+ },
+ {
+ "file": "QBC9fsq6QRw.wav",
+ "details": {
+ "title": "Take me back",
+ "artist": "HAIM",
+ "video_id": "QBC9fsq6QRw",
+ "duration": "3:46",
+ "thumbnail": "https://i.ytimg.com/vi/QBC9fsq6QRw/maxresdefault.jpg",
+ "channel_id": "UCxUTLbpwMNwO6g8kx-BHdQg"
+ }
+ },
+ {
+ "file": "BEOZDnZQSW0.wav",
+ "details": {
+ "title": "Cause = Time",
+ "artist": "Middle Kids",
+ "video_id": "BEOZDnZQSW0",
+ "duration": "4:57",
+ "thumbnail": "https://i.ytimg.com/vi/BEOZDnZQSW0/maxresdefault.jpg",
+ "channel_id": "UCUZZ4hnrbr9uXLuihRYXRNA"
+ }
+ },
+ {
+ "file": "e1RuKNNA19o.wav",
+ "details": {
+ "title": "Break This Heart",
+ "artist": "W O L F C L U B",
+ "video_id": "e1RuKNNA19o",
+ "duration": "3:41",
+ "thumbnail": "https://i.ytimg.com/vi/e1RuKNNA19o/maxresdefault.jpg",
+ "channel_id": "UCt1XzuTdfAv8Lbx5YRovcZQ"
+ }
+ },
+ {
+ "file": "4IYUb1O13uY.wav",
+ "details": {
+ "title": "Light Me Up",
+ "artist": "Adventure Club",
+ "video_id": "4IYUb1O13uY",
+ "duration": "2:51",
+ "thumbnail": "https://i.ytimg.com/vi/4IYUb1O13uY/maxresdefault.jpg",
+ "channel_id": "UCKo2HYwW-g0RPJ8zvU0CcUg"
+ }
+ },
+ {
+ "file": "VoGSv02_vKw.wav",
+ "details": {
+ "title": "Alice",
+ "artist": "mcbaise",
+ "video_id": "VoGSv02_vKw",
+ "duration": "5:21",
+ "thumbnail": "https://i.ytimg.com/vi/VoGSv02_vKw/maxresdefault.jpg",
+ "channel_id": "UC2OpW_vHl4QmOY0XlDJ9c0Q"
+ }
+ },
+ {
+ "file": "S_V1l_OKksM.wav",
+ "details": {
+ "title": "She Is (stripped)",
+ "artist": "Soccer Mommy",
+ "video_id": "S_V1l_OKksM",
+ "duration": "3:53",
+ "thumbnail": "https://i.ytimg.com/vi/S_V1l_OKksM/maxresdefault.jpg",
+ "channel_id": "UCiUjSTW94mp2xfW9jqotQlg"
+ }
+ },
+ {
+ "file": "WNcFERh0KEE.wav",
+ "details": {
+ "title": "Carried Away",
+ "artist": "Men I Trust",
+ "video_id": "WNcFERh0KEE",
+ "duration": "3:28",
+ "thumbnail": "https://i.ytimg.com/vi/WNcFERh0KEE/maxresdefault.jpg",
+ "channel_id": "UCaPOa_Tg0TThuYSVtUEXb8g"
+ }
+ },
+ {
+ "file": "M2K8sB8y-v4.wav",
+ "details": {
+ "title": "Da Nang",
+ "artist": "Tokyo Tea Room",
+ "video_id": "M2K8sB8y-v4",
+ "duration": "2:42",
+ "thumbnail": "https://i.ytimg.com/vi/M2K8sB8y-v4/maxresdefault.jpg",
+ "channel_id": "UCuXNHrFk7jDs-s3Nl-t5_rA"
+ }
+ },
+ {
+ "file": "39ZQ5BB2fGw.wav",
+ "details": {
+ "title": "Cool",
+ "artist": "Hotel Pools",
+ "video_id": "39ZQ5BB2fGw",
+ "duration": "3:00",
+ "thumbnail": "https://i.ytimg.com/vi/39ZQ5BB2fGw/maxresdefault.jpg",
+ "channel_id": "UCHNQ_4_csObrqJ9p7q-Xxgg"
+ }
+ },
+ {
+ "file": "jDqtgQvkRPU.wav",
+ "details": {
+ "title": "Holding On",
+ "artist": "Mint Julep",
+ "video_id": "jDqtgQvkRPU",
+ "duration": "4:23",
+ "thumbnail": "https://i.ytimg.com/vi/jDqtgQvkRPU/maxresdefault.jpg",
+ "channel_id": "UCSaPP3FUdsecX6gnkbI-Gug"
+ }
+ },
+ {
+ "file": "kV6MLh49Gu4.wav",
+ "details": {
+ "title": "Hold",
+ "artist": "Foudroie",
+ "video_id": "kV6MLh49Gu4",
+ "duration": "3:55",
+ "thumbnail": "https://i.ytimg.com/vi/kV6MLh49Gu4/maxresdefault.jpg",
+ "channel_id": "UCh8Ejj14iUMScX56373Q5Sw"
+ }
+ },
+ {
+ "file": "MsknANnaZ10.wav",
+ "details": {
+ "title": "Prophecy",
+ "artist": "Creo",
+ "video_id": "MsknANnaZ10",
+ "duration": "2:47",
+ "thumbnail": "https://i.ytimg.com/vi/MsknANnaZ10/maxresdefault.jpg",
+ "channel_id": "UCVilBDWd-gHrw84XEZi3EMQ"
+ }
+ },
+ {
+ "file": "Lk916K9QmnI.wav",
+ "details": {
+ "title": "Luminous",
+ "artist": "Emil Rottmayer",
+ "video_id": "Lk916K9QmnI",
+ "duration": "4:00",
+ "thumbnail": "https://i.ytimg.com/vi/Lk916K9QmnI/maxresdefault.jpg",
+ "channel_id": "UCHagumuO_g8dwLmYqNxlTKA"
+ }
+ }
+ ]
+}
playlist/2025-06/playlist.json
@@ -0,0 +1,150 @@
+{
+ "id": "PL1q1SH2wELHDDNwkVRcHDl80NLRdsjkMH",
+ "title": "singles - 2025-06",
+ "playlist": [
+ {
+ "file": "4cd_upwh93A.wav",
+ "details": {
+ "title": "Gimmicks",
+ "artist": "GRACEY",
+ "video_id": "4cd_upwh93A",
+ "duration": "3:18",
+ "thumbnail": "https://i.ytimg.com/vi/4cd_upwh93A/maxresdefault.jpg",
+ "channel_id": "UC9Z7V4P6D-6Yj9ApErTa0TQ"
+ }
+ },
+ {
+ "file": "APvEk-IEGJ4.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "Fame is a Gun",
+ "artist": "Addison Rae",
+ "video_id": "APvEk-IEGJ4",
+ "duration": "3:04",
+ "thumbnail": "https://i.ytimg.com/vi/APvEk-IEGJ4/maxresdefault.jpg",
+ "channel_id": "UCYcpWlYcu5q3OPbpgP_0Usw"
+ }
+ },
+ {
+ "file": "KRG520Y3rvM.wav",
+ "details": {
+ "title": "dirty martini",
+ "artist": "salem ilese",
+ "video_id": "KRG520Y3rvM",
+ "duration": "1:41",
+ "thumbnail": "https://i.ytimg.com/vi/KRG520Y3rvM/maxresdefault.jpg",
+ "channel_id": "UCpL0a0o0Iw2s75s0-z_KHKg"
+ }
+ },
+ {
+ "file": "pAVFSp5zBqc.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "Manchild",
+ "artist": "Sabrina Carpenter",
+ "video_id": "pAVFSp5zBqc",
+ "duration": "3:34",
+ "thumbnail": "https://i.ytimg.com/vi/pAVFSp5zBqc/maxresdefault.jpg",
+ "channel_id": "UCz51ZodJbYUNfkdPHOjJKKw"
+ }
+ },
+ {
+ "fade": 5.0,
+ "file": "8mbdiBlW6lw.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "At Least I'd Be A Cowboy!",
+ "artist": "Madilyn Mei",
+ "video_id": "8mbdiBlW6lw",
+ "duration": "3:24",
+ "thumbnail": "https://i.ytimg.com/vi/8mbdiBlW6lw/maxresdefault.jpg",
+ "channel_id": "UCCpPkTyWCzmzERSuZ0GystQ"
+ }
+ },
+ {
+ "file": "dcg_nrP3jF8.wav",
+ "trim_end": 1.0,
+ "details": {
+ "title": "High Speed Chasing",
+ "artist": "BØRNS",
+ "video_id": "dcg_nrP3jF8",
+ "duration": "3:19",
+ "thumbnail": "https://i.ytimg.com/vi/dcg_nrP3jF8/maxresdefault.jpg",
+ "channel_id": "UC57yfhZw9tV1Wd7h7HArc7Q"
+ }
+ },
+ {
+ "file": "pWv0wKueFY4.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "Diet Pepsi (Live at Sirius XMU)",
+ "artist": "Blondshell",
+ "video_id": "pWv0wKueFY4",
+ "duration": "2:46",
+ "thumbnail": "https://i.ytimg.com/vi/pWv0wKueFY4/maxresdefault.jpg",
+ "channel_id": "UC6Ziy1PmgqE6O1lFwyJp1dg"
+ }
+ },
+ {
+ "file": "MICNo_kF3CU.wav",
+ "trim_end": 1.0,
+ "details": {
+ "title": "Sexy to Someone (BBC Live from Maida Vale)",
+ "artist": "Suki Waterhouse",
+ "video_id": "MICNo_kF3CU",
+ "duration": "3:36",
+ "thumbnail": "https://i.ytimg.com/vi/MICNo_kF3CU/maxresdefault.jpg",
+ "channel_id": "UCeRJ4IHCfysUxLq1fRnXCeQ"
+ }
+ },
+ {
+ "file": "cLj0UkRdQN0.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "My Baby (Got Nothing At All) (Materialists Original Soundtrack)",
+ "artist": "Japanese Breakfast",
+ "video_id": "cLj0UkRdQN0",
+ "duration": "4:02",
+ "thumbnail": "https://i.ytimg.com/vi/cLj0UkRdQN0/maxresdefault.jpg",
+ "channel_id": "UCYgfTCJgqA23D29kxRIRgLQ"
+ }
+ },
+ {
+ "file": "u2Yk5GtDdrI.wav",
+ "trim_end": 3.0,
+ "details": {
+ "title": "M (stripped)",
+ "artist": "Soccer Mommy",
+ "video_id": "u2Yk5GtDdrI",
+ "duration": "3:31",
+ "thumbnail": "https://i.ytimg.com/vi/u2Yk5GtDdrI/maxresdefault.jpg",
+ "channel_id": "UCiUjSTW94mp2xfW9jqotQlg"
+ }
+ },
+ {
+ "fade": 3.0,
+ "file": "97Fw9gJKjCY.wav",
+ "trim_end": 2.0,
+ "details": {
+ "title": "Crosses (Bibio Remix)",
+ "artist": "José González",
+ "video_id": "97Fw9gJKjCY",
+ "duration": "3:01",
+ "thumbnail": "https://i.ytimg.com/vi/97Fw9gJKjCY/maxresdefault.jpg",
+ "channel_id": "UCVKb7VKgr2i5X5cFZCF2gig"
+ }
+ },
+ {
+ "fade": 1.0,
+ "file": "kFfZhrIlTno.wav",
+ "details": {
+ "title": "Nettles",
+ "artist": "Ethel Cain",
+ "video_id": "kFfZhrIlTno",
+ "duration": "8:04",
+ "thumbnail": "https://i.ytimg.com/vi/kFfZhrIlTno/maxresdefault.jpg",
+ "channel_id": "UCBbh9rRZc-Qm4Dk5CJFbFng"
+ }
+ }
+ ]
+}
.gitignore
@@ -0,0 +1,2 @@
+*.wav
+*.ogg
fetch.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/bryfry/singles/internal/playlist"
+)
+
+func main() {
+
+ if len(os.Args) < 2 {
+ err := fmt.Errorf("Usage: go run fetch.go <path to playlist.json>")
+ log.Fatal(err)
+ }
+ playlistPath := os.Args[1]
+ playlistDir := filepath.Dir(playlistPath)
+ f, err := os.Open(playlistPath)
+ if err != nil {
+ err = fmt.Errorf("opening playlist: %w", err)
+ log.Fatal(err)
+ }
+
+ var pl playlist.Playlist
+ d := json.NewDecoder(f)
+ err = d.Decode(&pl)
+ if err != nil {
+ err = fmt.Errorf("decoding playlist: %w", err)
+ log.Fatal(err)
+ }
+
+ playlistURL := fmt.Sprintf(
+ "https://music.youtube.com/playlist?list=%s",
+ pl.Id,
+ )
+
+ yt := "yt-dlp"
+ ytPath, err := exec.LookPath(yt)
+ if err != nil {
+ err = fmt.Errorf("%s not found in PATH %w", yt, err)
+ log.Fatal(err)
+ }
+
+ cmd := exec.Command(
+ ytPath,
+ "--extract-audio",
+ "--audio-format", "wav",
+ "--paths", playlistDir,
+ "--output", "%(id)s.%(ext)s",
+ "--restrict-filenames",
+ playlistURL,
+ )
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ err = cmd.Run()
+ if err != nil {
+ err = fmt.Errorf("running yt-dlp: %w", err)
+ log.Fatal(err)
+ }
+}
go.mod
@@ -0,0 +1,35 @@
+module github.com/bryfry/singles
+
+go 1.24.2
+
+require (
+ github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
+ google.golang.org/api v0.239.0
+)
+
+require (
+ cloud.google.com/go/auth v0.16.2 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+ cloud.google.com/go/compute/metadata v0.7.0 // indirect
+ github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.2 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
+ go.opentelemetry.io/otel v1.36.0 // indirect
+ go.opentelemetry.io/otel/metric v1.36.0 // indirect
+ go.opentelemetry.io/otel/trace v1.36.0 // indirect
+ golang.org/x/crypto v0.39.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
+ golang.org/x/oauth2 v0.30.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.26.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+ google.golang.org/grpc v1.73.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+)
go.sum
@@ -0,0 +1,75 @@
+cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
+cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
+cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
+github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 h1:o64h9XF42kVEUuhuer2ehqrlX8rZmvQSU0+Vpj1rF6Q=
+github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61/go.mod h1:Rp8e0DCtEKwXFOC6JPJQVTz8tuGoGvw6Xfexggh/ed0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
+github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
+github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 h1:dd7vnTDfjtwCETZDrRe+GPYNLA1jBtbZeyfyE8eZCyk=
+github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
+go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
+go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
+google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
+google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
+google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
+google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mixtape.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/bryfry/singles/internal/playlist"
+ "github.com/bryfry/singles/internal/wav"
+)
+
+func main() {
+
+ if len(os.Args) < 2 {
+ err := fmt.Errorf("Usage: go run mixtape.go <path to playlist.json>")
+ log.Fatal(err)
+ }
+ playlistPath := os.Args[1]
+ playlistDir := filepath.Dir(playlistPath)
+ f, err := os.Open(playlistPath)
+ if err != nil {
+ err = fmt.Errorf("opening playlist: %w", err)
+ log.Fatal(err)
+ }
+
+ var pl playlist.Playlist
+ d := json.NewDecoder(f)
+ err = d.Decode(&pl)
+ if err != nil {
+ err = fmt.Errorf("decoding playlist: %w", err)
+ log.Fatal(err)
+ }
+
+ var combined *wav.WAV
+ for i, t := range pl.Tracks {
+
+ tPath := filepath.Join(playlistDir, t.Filename)
+ track, err := wav.ReadWAV(tPath)
+ if err != nil {
+ panic(err)
+ }
+ if combined == nil {
+ combined = track
+ continue
+ }
+ if t.TrimEnd != 0 {
+ track.TrimEnd(t.TrimEnd)
+ }
+ if t.TrimStart != 0 {
+ track.TrimStart(t.TrimStart)
+ }
+ var transition *wav.WAV
+ if t.Fade != 0 {
+ transition, err = combined.Fade(track, t.Fade)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ transition, err = combined.Append(track)
+ if err != nil {
+ panic(err)
+ }
+ }
+ transitionFileName := fmt.Sprintf("%02d_transition.wav", i)
+ transitionPath := filepath.Join(playlistDir, transitionFileName)
+ err = transition.WriteFile(transitionPath)
+ if err != nil {
+ panic(err)
+ }
+
+ }
+
+ mixtapeWav := "mixtape.wav"
+ mixtapeWavPath := filepath.Join(playlistDir, mixtapeWav)
+ err = combined.WriteFile(mixtapeWavPath)
+ if err != nil {
+ panic(err)
+ }
+
+ ffmpeg := "ffmpeg"
+ ffmpegPath, err := exec.LookPath(ffmpeg)
+ if err != nil {
+ err = fmt.Errorf("%s not found in PATH %w", ffmpeg, err)
+ log.Fatal(err)
+ }
+
+ mixtapeOgg := "mixtape.ogg"
+ mixtapeOggPath := filepath.Join(playlistDir, mixtapeOgg)
+ cmd := exec.Command(
+ ffmpegPath,
+ "-i", mixtapeWavPath,
+ "-c:a", "libopus",
+ "-b:a", "128k",
+ "-vbr", "on",
+ "-application", "audio",
+ mixtapeOggPath,
+ )
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ err = cmd.Run()
+ if err != nil {
+ err = fmt.Errorf("running %s: %w", ffmpeg, err)
+ log.Fatal(err)
+ }
+}
playlist.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/bryfry/singles/internal/playlist"
+)
+
+func main() {
+
+ apiKey, apiKeyFound := os.LookupEnv("YOUTUBE_API_KEY")
+ if !apiKeyFound {
+ log.Fatal("YOUTUBE_API_KEY environment variable is not set")
+ }
+
+ if len(os.Args) < 2 {
+ log.Fatal("Usage: go run main.go <playlist_id>")
+ }
+ playlistID := os.Args[1]
+
+ if strings.Contains(playlistID, "youtube") {
+ parsed, err := url.Parse(playlistID)
+ if err != nil {
+ log.Fatal("parsing url")
+ }
+ q := parsed.Query()
+ id := q.Get("list")
+ if id == "" {
+ log.Fatal("list parameter not found in url")
+ }
+ playlistID = id
+ }
+
+ pl, err := playlist.LookupPlaylist(apiKey, playlistID)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ dirName, _ := strings.CutPrefix(pl.Title, "singles - ")
+ playlistPath := filepath.Join("playlist", dirName)
+ err = os.MkdirAll(playlistPath, 0o755)
+ if err != nil {
+ err = fmt.Errorf("creating playlist directory: %w", err)
+ log.Fatal(err)
+ }
+
+ metadataPath := filepath.Join(playlistPath, "playlist.json")
+ f, err := os.OpenFile(
+ metadataPath,
+ os.O_CREATE|os.O_EXCL|os.O_WRONLY,
+ 0o655)
+ if err != nil {
+ err = fmt.Errorf("opening playlist metadata file: %w", err)
+ log.Fatal(err)
+ }
+
+ enc := json.NewEncoder(f)
+ enc.SetIndent("", " ")
+ err = enc.Encode(pl)
+ if err != nil {
+ err = fmt.Errorf("opening playlist metadata file: %w", err)
+ log.Fatal(err)
+ }
+}
README.md
@@ -0,0 +1,26 @@
+# `singles`
+
+## Use
+
+```bash
+# collect playlist details
+go run playlist.go "{{ PLAYLIST_ID || PLAYLIST_URL }}"
+```
+
+```bash
+# collect wav files of playlist
+go run fetch.go {{ path to playlist.json }}
+```
+
+```bash
+# build mixtape
+go run mixtape.go {{ path to playlist.json }}
+```
+
+
+### TODO
+- [x] get playlist details into JSON
+ - [x] playlist title
+- [x] run yt-dlp on a playlist.json
+- [ ] progress details mixtape.go
+- [ ] make cobra-cli with subcommands