sqlite music library table creation script added

main
Rob Canning 2024-01-18 10:51:12 +01:00
parent 3853e1544b
commit cc1c3a8507
13 changed files with 984 additions and 861 deletions

Binary file not shown.

View File

@ -0,0 +1,74 @@
#!/usr/bin/python3
import sys, os, datetime, fnmatch, glob, random, time, pathlib, re
from datetime import timedelta
from os.path import join
from tinytag import TinyTag
from random import shuffle
import sqlite3, json
import uuid
conn = ''
music_library_path = "/home/rob/antena/music/"
conn = sqlite3.connect("database/show.db")
labelnames = ["sploh", "terraformer", "pharamafabric" ]
def database_create(conn):
# the MUSIC LIBRARY TABLE
conn.execute('''CREATE TABLE IF NOT EXISTS MUSICLIB(
ID INTEGER PRIMARY KEY AUTOINCREMENT,
LABEL TEXT NOT NULL,
ALBUM TEXT ,
TRACK TEXT ,
ARTIST TEXT ,
GENRE TEXT ,
TRACKDUR TEXT ,
YEAR TEXT,
URL TEXT,
PATH TEXT ,
LASTPLAY TEXT,
COMMENT TEXT
);''')
print("Table created successfully");
#######################################################
def mk_db_entry(conn):
import os
label_url = "https://fixme.bandcamp.com/"
for subdir, dirs, files in os.walk(music_library_path):
for file in files:
for m in [".flac", ".FLAC", ".mp3"]: # get audio files
if m in file:
filepath = pathlib.Path(subdir + "/" +file)
label = filepath.parts[5] # get LABEL etc name from path
track = TinyTag.get(os.path.join(subdir, file))# get metadata from file
cursor = conn.cursor()
cursor.execute("INSERT INTO MUSICLIB (\
ID, LABEL, ALBUM, TRACK, ARTIST,\
GENRE, TRACKDUR, YEAR, URL, PATH, LASTPLAY, COMMENT) \
VALUES (NULL, ?, ?, ?, ?,?, ?,?,?,?,?,? )",\
[label, track.album, track.title, track.artist, \
track.genre, track.duration, track.year, label_url, str(filepath), "last_played", track.comment]);
conn.commit()
# print("DB Record created successfully");
# track = TinyTag.get(i)
# detail = str(track.artist) + " | " + str(track.album) + \
# " | " + str(track.title) + " | " + str(track.year) + \
# " | " + str(timedelta(seconds=round(track.duration)))
# show_info.append("" + detail)
#def insert_track(conn, track):
database_create(conn)
mk_db_entry(conn)

View File

@ -8,25 +8,25 @@ from random import shuffle
import sqlite3, json
import uuid
from mk_web import *
#from mk_web import *
path = "/home/rob/antena/"
show_name = "The_SLO_Indie"
show_short_description = "An eclectic selection of independent and experimental music from Slovenia. \
Published weekly by The Rizoma Institute. Hosted and compiled by Rob Canning. \
Broadcast in Slovenia on FM by Mariborski radio student - MARŠ"
episode_number = sys.argv[1]
episode_author="Rob Canning"
episode_number = int(sys.argv[1])
input_date = sys.argv[2]
episode_date = datetime.datetime.now().strftime("%Y-%m-%d")
episode_duration = 0
complete_playlist = []
show_array = []
episode_playlist = []
show_cover = ""
archive = []
artists_played = []
artist_abreviated = []
show_name = "The_SLO_Indie" # OBED
path = "/home/rob/antena/"
web_path = "/home/rob/antena/html/episode/{0}/img".format(episode_number)
if os.path.exists(web_path):
@ -57,7 +57,7 @@ def database_create():
ID INTEGER PRIMARY KEY AUTOINCREMENT,
EPISODE INT NOT NULL,
DATE TEXT NOT NULL,
ALBUM TEXT NOT NULL,
ALBUM TEXT ,
TRACK TEXT NOT NULL,
ARTIST TEXT NOT NULL,
TRACKDUR INT NOT NULL,
@ -67,16 +67,10 @@ def database_create():
print("Table created successfully");
#conn.execute("INSERT INTO SHOW (ID, EPISODE,DATE,TRACK,ARTIST) \
#VALUES (1, 1, 'wednesday', 'Some song', 'Some Artist' )");
#conn.commit()
#print("DB Record created successfully");
#conn.close()
def create_intro(show_array):
intropath = path + "texts/clips/this_is"
def create_intro(episode_playlist):
intropath = path + "audio/texts/clips/this_is"
intro = random.choice(os.listdir(intropath))
show_array.insert(0, str(os.path.abspath(intropath)) + "/" + str(intro))
episode_playlist.insert(0, str(os.path.abspath(intropath)) + "/" + str(intro))
def add_to_tracks_played(add_to_played: str):
with open('playlists/track_playout_history.txt', "a") as tracks_played_file:
@ -105,7 +99,7 @@ def load_all_music():
'''.format(str(len(complete_playlist))))
return complete_playlist
def create_show_playlist(show_array: list, complete_playlist:list):
def create_episode_playlist(episode_playlist: list, complete_playlist:list):
load_all_music()
global episode_duration
@ -113,10 +107,12 @@ def create_show_playlist(show_array: list, complete_playlist:list):
track_count = 0
global artists_played
max_track_dur = 10
max_track_dur = 9
min_track_dur = 2
while episode_duration < 60 * 60 and track_count < 10 :
# TODO what is most important 12 tracks or 60 minutes
while episode_duration < 60 * 60 and track_count < 12 :
song = random.choice(random.sample(\
complete_playlist, len(complete_playlist) )).rstrip() # pick a song
track = TinyTag.get(song) # get its metadata
@ -126,7 +122,7 @@ def create_show_playlist(show_array: list, complete_playlist:list):
if check_archive(track.title) is True:
if int(track.duration) > min_track_dur * 60:
if int(track.duration) < max_track_dur * 60:
show_array.append(song.rstrip()) # if 'not in' is true then add the song
episode_playlist.append(song.rstrip()) # if 'not in' is true then add the song
art = string=re.sub("\(.*?\)","",track.artist)
# shorten verbose artist names such as trojnik Trojnik (Cene Resnik, Tomaž Grom, Vid Drašler)
art = string=re.sub("and","&",art)
@ -139,6 +135,11 @@ def create_show_playlist(show_array: list, complete_playlist:list):
cursor = conn.cursor()
#long_string = json.dumps(["' SomeWord"])
print([episode_number, episode_date, \
track.album, track.title, track.artist, \
track.duration, track.year, track_path])
cursor.execute("INSERT INTO SHOW (\
ID, EPISODE, DATE, ALBUM, TRACK, ARTIST, TRACKDUR, YEAR, PATH) \
VALUES (NULL, ?, ?, ?, ?, ?,?,?,? )", [episode_number, episode_date, \
@ -147,10 +148,6 @@ def create_show_playlist(show_array: list, complete_playlist:list):
conn.commit()
print("DB Record created successfully");
track_count += 1; print(track_count)
episode_duration = episode_duration + track.duration
else: print("TRACK TOO SHORT..........." )
@ -164,7 +161,7 @@ def create_show_playlist(show_array: list, complete_playlist:list):
print("total tracks = {0} \n total duration = {1} ".format(track_count, episode_duration))
return show_array, episode_duration
return episode_playlist, episode_duration
def combine_images(columns, space, images, variants:int):
global show_cover
@ -192,6 +189,7 @@ def combine_images(columns, space, images, variants:int):
imga = Image.open(image)
size = (500,500)
img = imga.resize(size)
#img = img.rotate(random.randrange(360))
x_offset = int((width_max-img.width)/2)
y_offset = int((height_max-img.height)/2)
background.paste(img, (x+x_offset, y+y_offset+100))
@ -210,15 +208,20 @@ def combine_images(columns, space, images, variants:int):
h2_baseline = 1530
# Add Text to the image ----------------------------------------
# -------------------------------------------------------------------
# some logic to shuffle the list if sub sections of list are too long for layout
str_length_thresh = 50
#TODO if an artist is listed as Various Arist then remove it from cover display and think of logic
#TODO exit while loop if it cant find a solution and restart script or shrink font and adjust rules
while \
[len(s) for s in [''.join(artist_abreviated[0:3])]][0] > str_length_thresh or\
[len(s) for s in [''.join(artist_abreviated[3:6])]][0] > str_length_thresh or\
[len(s) for s in [''.join(artist_abreviated[6:9])]][0] > str_length_thresh or \
[len(s) for s in [''.join(artist_abreviated[9:12])]][0] > str_length_thresh:
print("on of the lines is longer than fits the page... shuffling the list for a better look")
print('''one of the lines is longer than fits the page...
...................shuffling the list for a better look''')
random.shuffle(artist_abreviated)
im.text((30,10), '''an eclectic selection of contemporary independent music from slovenia: {0} - E P I S O D E #{1}
@ -242,11 +245,11 @@ def combine_images(columns, space, images, variants:int):
background.save("html/" + "episode/{0}/{1}".format(episode_number, show_cover))
return show_cover
def create_show_coverart(show_array, variants):
def create_show_coverart(episode_playlist, variants):
show_cover_jpgs = []
# in the directory containing songs find jpg and pngs containing string "cover"
for dir in show_array:
for dir in episode_playlist:
path = pathlib.Path(dir).parent
for file in os.listdir(path):
for p in [".png", ".jpg", ".jpeg"]:
@ -255,16 +258,14 @@ def create_show_coverart(show_array, variants):
if i in file:
show_cover_jpgs.append(str(path) + "/" + file)
print('''
------------------------
print(''''\n ------------------------
creating show cover art
------------------------
''')
------------------------ ''')
# if len(show_cover_jpgs) > 0: # duplicate this for variations of geometry
for i in range(variants):
x = show_cover_jpgs[:12]
combine_images(columns=4, space=3, images=random.sample(x,len(x)),variants=i)
combine_images(columns=4, space=10, images=random.sample(x,len(x)),variants=i)
return show_cover
@ -294,19 +295,18 @@ def create_animated_gif():
def create_pls_file():
# write the selection as a playlist file
with open("shows/antena_playlist_" + episode_date + ".pls","w") as file:
file.writelines("\n".join(show_array))
file.writelines("\n".join(episode_playlist))
def create_podcast(show_array: list):
def create_podcast(episode_playlist: list):
print('''
------------------------
print('''------------------------
creating show audio
------------------------''')
from glob import glob
from pydub import AudioSegment
playlist_songs = [AudioSegment.from_file(flac_file) for flac_file in show_array]
playlist_songs = [AudioSegment.from_file(flac_file) for flac_file in episode_playlist]
print(playlist_songs)
show_intro = playlist_songs[0]
# first_song = playlist_songs[0].fade_in(0) # only fadein if used over show intro - currently not used
@ -369,71 +369,21 @@ def create_podcast(show_array: list):
print("MP3 audio file exported...")
### ------------------------------------------------------------
def create_html_episode_from_template():
from jinja2 import Template, Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('html/templates'))
episode_template = env.get_template('episode.jinja')
show_info = []
# maybe a jinja2 template loop here instead
for i in show_array:
track = TinyTag.get(i)
detail = str(track.artist) + " | " + str(track.album) + \
" | " + str(track.title) + " | " + str(track.year) + \
" | " + str(timedelta(seconds=round(track.duration)))
show_info.append("" + detail)
output_from_parsed_template = episode_template.render(episode_author="rrrrrrrr",\
episode_duration=episode_duration, episode_date=episode_date, \
about_show=show_short_description, episode_playlist=show_info, \
episode_image="img/cover.png".format(episode_number))
with open("html/episode/{0}/index.html".format(episode_number), "w") as episode_page:
episode_page.write(output_from_parsed_template)
def create_html_episode_from_template():
from jinja2 import Template, Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('html/templates'))
episode_template = env.get_template('episode.jinja')
show_info = []
# maybe a jinja2 template loop here instead
for i in show_array:
track = TinyTag.get(i)
detail = str(track.artist) + " | " + str(track.album) + \
" | " + str(track.title) + " | " + str(track.year) + \
" | " + str(timedelta(seconds=round(track.duration)))
show_info.append("" + detail)
output_from_parsed_template = episode_template.render(episode_author="rrrrrrrr",\
episode_duration="123", about_show=show_short_description, episode_playlist=show_info, \
episode_image="img/cover.png".format(episode_number))
with open("html/episode/{0}/index.html".format(episode_number), "w") as episode_page:
episode_page.write(output_from_parsed_template)
def create_html_homepage_from_template():
def create_html_homepage_from_template(episode_playlist):
# TODO "on this weeks show" varients fed to html from list here
from jinja2 import Template, Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('html/templates'))
homepage_template = env.get_template('homepage.jinja')
conn = sqlite3.connect("database/show.db")
cur = conn.cursor()
show_info = []
episode_artists = []
for i in range(10):
artists = []
conn = sqlite3.connect("database/show.db")
cur = conn.cursor()
cur.execute("SELECT ARTIST FROM SHOW WHERE EPISODE=?", [i])
rows = cur.fetchall()
@ -445,8 +395,8 @@ def create_html_homepage_from_template():
episode_artists.append(artists)
episodes = []
for i in range(10):
print(episode_artists)
for i in range(10): # get this from new table EPISODE_STATS number of tracks in DB
print(episode_artists[i])
an_episode = dict(date="2012-02-", \
episode_artists=episode_artists[i], episode_number=i, \
episode_date=episode_date, \
@ -454,9 +404,13 @@ def create_html_homepage_from_template():
episodes.append(an_episode)
episodes = reversed(episodes) # reversed order to most recent episode appears first in list
#TODO get database sorted - music table with metadata, bandcamp, label info
cur.execute('SELECT * FROM SHOW WHERE EPISODE=?', [episode_number])
# database query instead of the below
# maybe a jinja2 template loop here instead
for i in show_array:
for i in episode_playlist: # liar od files
#for i in cur: # liar od files
track = TinyTag.get(i)
detail = str(track.artist) + " | " + str(track.album) + \
" | " + str(track.title) + " | " + str(track.year) + \
@ -467,7 +421,7 @@ def create_html_homepage_from_template():
show_name=''.join(random.choice((str.upper,str.lower))(x) for x in show_name), \
episodes=episodes, episode_author="Rob Canning",\
episode_duration=episode_duration, episode_number=episode_number, \
episode_artists=episode_artists, \
episode_artists=episode_artists[episode_number], \
about_show=show_short_description, episode_playlist=show_info, \
episode_image="episode/{0}/img/cover.png".format(episode_number))
@ -475,7 +429,7 @@ def create_html_homepage_from_template():
episode_page.write(output_from_parsed_template)
def create_html_episode_from_template():
def create_html_episode_from_template(episode_playlist):
from jinja2 import Template, Environment, FileSystemLoader
@ -485,7 +439,7 @@ def create_html_episode_from_template():
show_info = []
# maybe a jinja2 template loop here instead
for i in show_array:
for i in episode_playlist:
track = TinyTag.get(i)
detail = str(track.artist) + " | " + str(track.album) + \
" | " + str(track.title) + " | " + str(track.year) + \
@ -502,8 +456,11 @@ def create_html_episode_from_template():
with open("html/episode/{0}/index.html".format(episode_number), "w") as episode_page:
episode_page.write(output_from_parsed_template)
def create_RSS_XML_from_template():
from jinja2 import Template, Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('html/templates'))
rss_template = env.get_template('show_RSS.jinja.xml')
output_from_parsed_template = rss_template.render(\
@ -520,19 +477,19 @@ def create_RSS_XML_from_template():
database_create()
set_episode_date(input_date)
create_show_playlist(show_array, complete_playlist)
create_show_coverart(show_array, 1) #episode_duration = 100
create_episode_playlist(episode_playlist, complete_playlist)
create_show_coverart(episode_playlist, 1) #episode_duration = 100
#create_animated_gif()
create_intro(show_array)
create_intro(episode_playlist)
create_pls_file()
create_html_episode_from_template()
create_html_homepage_from_template()
create_html_episode_from_template(episode_playlist)
create_html_homepage_from_template(episode_playlist)
create_RSS_XML_from_template()
#create_podcast(show_array)
#create_podcast(episode_playlist)
#convert -delay 100 -loop 0 html/episode/2/img/show_cover_2024-01-12* animatedGIF.gif

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
make this title easy to remember
Tomaž Grom - Sam, za... - 05 G.V.
Magie de la Pleine Lune
Untold Truths
Ultrascan
1disrupt3violence1monopoly2
ejk
Zlatko Kaučič, Tomaž Grom - Torn Memories Of Folklore - Raztrgana folklora spomina - 05 With A Hole In The Sock
Action VI
Action XII
Speshlafer
Tomaž Grom - Sam, za... - 09 N.N.
YuWrong
Vertex
Carve4
Dromon
Blue Waters Turn Black (P.C.M. remix)
Cifre (feat. Eno)
Lynch - Oro zatvorenih očiju
KIURIKI - Killem All
postcell (Tetsuo remix)
Stampede

View File

View File

@ -1,9 +1,25 @@
#!/bin/bash
# flush everything
#echo "" > playlists/track_playout_history.txt; rm database/show.db ; mk_master_playlist.sh; ./mk_show.py 4 2024-04-17;
rm html/index.html
for i in [2024-03-03 2024-03-10 2024-03-17 2024-03-24 2024-03-31]
do echo mk_master_playlist.sh; ./mk_show.py $i
show_dates=(2024-03-03 2024-03-10 2024-03-17 2024-03-24 2024-03-31 2024-03-31 2024-03-31 2024-03-31 2024-03-31 )
counter= # set this as input argument to set what episode to start creating from...
episodes=12 # set this as another input argument for how many episodes to produce
for i in "${show_dates[@]}"
do
if [i==0]; then
echo "" > playlists/track_playout_history.txt; rm database/show.db ; mk_master_playlist.sh; ./mk_show.py $counter $i ;
((counter++));
else
echo mk_master_playlist.sh; ./mk_show.py $counter $i ;
((counter++));
fi
done

View File

@ -1,15 +0,0 @@
/home/rizoma/antena/music/kam/Warrego_Valles_-_Botox_(2018_Kamizdat)_[FLAC]/6. Warrego Valles - Madmen.flac
/home/rizoma/antena/music/kam/Dodecahedragraph - Mixtures and Leftovers (2018, Kamizdat, FLAC)/20 - annihilation_var. 0.flac
/home/rizoma/antena/music/kam/Shekuza - De Sica (2019, Kamizdat) [FLAC]/05 Shekuza - Nost.flac
/home/rizoma/antena/music/kam/Shekuza - De Sica (2019, Kamizdat) [FLAC]/04 Shekuza - Nebulae Tree.flac
/home/rizoma/antena/music/kam/Access Frame - Equity (Kamizdat, 2020) [FLAC]/Kamizdat - Access Frame- Equity - 16 Luka Prinčič- Tomorrow is Non Sequitur (For 3 Tape Players and More Bodies).flac
/home/rizoma/antena/music/kam/Warrego Valles - save as (Kamizdat, 2019) [FLAC]/3. Warrego Valles - make this title easy to remember.flac
/home/rizoma/antena/music/kam/Luka_Prinčič_-_Antigone_Child_(2018, Kamizdat).[FLAC]/12 The Mother (Act VII).flac
/home/rizoma/antena/music/kam/Shekuza - Coriolis Effect (Kamizdat, 2020) [FLAC]/02 Shekuza - Sand Mantis.flac
/home/rizoma/antena/music/kam/Warrego Valles - save as (Kamizdat, 2019) [FLAC]/5. Warrego Valles - mansplainer rehab.flac
/home/rizoma/antena/music/kam/Access Frame - Equity (Kamizdat, 2020) [FLAC]/Kamizdat - Access Frame- Equity - 11 #DUF- YuWrong.flac
/home/rizoma/antena/music/kam/Luka_Prinčič_-_Antigone_Child_(2018, Kamizdat).[FLAC]/06 Scena Bass Alto Soli (Outtake).flac
/home/rizoma/antena/music/kam/The Feminalz - Technoburlesque: Image Snatchers 3 (2022, Kamizdat) [FLAC]/The Feminalz - Technoburlesque Image Snatchers 3 - 4 Gucci Guči (feat GlitterAid).flac
/home/rizoma/antena/music/kam/Access Frame - Collectivity (Kamizdat, 2018) [FLAC]/09 Gašper Torkar - Inchoate Crimes.flac
/home/rizoma/antena/music/kam/Dodecahedragraph - Mixtures and Leftovers (2018, Kamizdat, FLAC)/04 - chan debris_var.2.flac
/home/rizoma/antena/music/kam/beepblip - Noise for Strings vol.2 (Kamizdat, 2020) [FLAC]/beepblip - Noise for Strings vol.2 - 04. Time travel.flac

View File

@ -1,11 +0,0 @@
/home/rob/antena/music/kamizdat/Irena Tomažin - Taste Of Silence/Irena Tomažin - Taste Of Silence - 02 Soil Stone.flac
/home/rob/antena/music/kamizdat/Gašper_Torkar_-_Accept_the_Risk_and_Continue_[Kamizdat_2021_FLAC]/Gašper_Torkar_-_Accept_the_Risk_and_Continue_-_4._Arboreal.flac
/home/rob/antena/music/pharmafabrik_recordings/Chris Wood - Misoneism/Chris Wood - Misoneism - 02 Quiet Certainty.flac
/home/rob/antena/music/sploh/Samo Kutin and Martin Küchen - Stutter And Strike/Samo Kutin and Martin Küchen - Stutter And Strike - 03 Svete gore.flac
/home/rob/antena/music/sploh/Axel Dörner and Tomaž Grom - Omejeno gibanje - Confined Movement/Axel Dörner and Tomaž Grom - Omejeno gibanje - Confined Movement - 06 Confined Movement- F.flac
/home/rob/antena/music/kamizdat/Shekuza - De Sica (2019, Kamizdat) [FLAC]/04 Shekuza - Nebulae Tree.flac
/home/rob/antena/music/sploh/Tone Pavček & Tomaž Grom - Sonce in sončice po vsem svetu/Tone Pavček & Tomaž Grom - Sonce in sončice po vsem svetu - 10 Ninulla Çapkanёve (Čenčačeva uspavanka).flac
/home/rob/antena/music/pharmafabrik_recordings/Various - Bora Scura Reimagined/Sunao Inami - Bora Scura Reimagined - 05 Action XV.flac
/home/rob/antena/music/kamizdat/Luka Prinčič + ala pecula - a space process (2022, Kamizdat) [FLAC]/Luka Prinčič + ala pecula - a space process - 06 - a1.flac
/home/rob/antena/music/kamizdat/beepblip - Noise for Strings vol.2 (Kamizdat, 2020) [FLAC]/beepblip - Noise for Strings vol.2 - 02. Protons.flac
/home/rob/antena/music/sploh/TiTiTi (Jure Boršič, Jošt Drašler, Vid Drašler) - Štafelaj/TiTiTi (Jure Boršič, Jošt Drašler, Vid Drašler) - Štafelaj - 02 Spone (Chains).flac

View File

@ -1,18 +0,0 @@
# SHOW INFO FOR ANTENA 2023-11-30
Total Playtime excluding fills 70.71117262849586
! Warrego Valles : Madmen : Botox : 2018 : 253.5
! Dodecahedragraph : annihilation_var. 0 : Mixtures and Leftovers : 2018 : 219.48065759637188
! Shekuza : Nost : De Sica : 2019 : 205.7599319727891
! Shekuza : Nebulae Tree : De Sica : 2019 : 234.95630385487527
! Luka Prinčič : Tomorrow is Non Sequitur (For 3 Tape Players and More Bodies) : Access Frame: Equity : 2020 : 326.2944217687075
! Warrego Valles : make this title easy to remember : save as : None : 351.2195238095238
! Luka Prinčič : The Mother (Act VII) : Antigone/Child : 2018 : 492.161201814059
! Shekuza : Sand Mantis : Coriolis Effect : 2020 : 241.11192743764173
! Warrego Valles : mansplainer rehab : save as : None : 301.3138775510204
! #DUF : YuWrong : Access Frame: Equity : 2020 : 230.13029478458049
! Luka Prinčič : Scena Bass Alto Soli (Outtake) : Antigone/Child : 2018 : 213.713537414966
! The Feminalz : Gucci Guči (feat GlitterAid) : Technoburlesque: Image Snatchers 3 : 2022 : 242.68108333333333
! Gašper Torkar : Inchoate Crimes : Access Frame - Collectivity : 2018 : 498.36061224489794
! Dodecahedragraph : chan debris_var.2 : Mixtures and Leftovers : 2018 : 116.98700680272108
! beepblip : Time travel : Noise for Strings vol.2 : 2020 : 314.99997732426306