A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Create seed file for app functions, such as following users, creating mixtapes and liking recordings. Some of the migrations had to be update to make them able to run.

oscar345 deff0bd6 2345dec4

+217 -64
+7
.gitignore
··· 8 8 **/node_modules 9 9 **/.zed 10 10 **/private/assets 11 + **/__pycache__ 12 + **/.pytest_cache 13 + **/.mypy_cache 14 + **/.ruff_cache 15 + **/.vscode 16 + **/.idea 17 + **/.
+3 -3
private/migrations/app/20260112144128_create_user_follows.sql
··· 7 7 updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 8 PRIMARY KEY (user_id, target_id), 9 9 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 10 - FOREIGN KEY (target_id) REFERENCES users(id) ON DELETE CASCADE, 10 + FOREIGN KEY (target_id) REFERENCES users(id) ON DELETE CASCADE 11 11 ); 12 12 13 - CREATE INDEX idx__user_follows__target_id ON followers(target_id); 14 - CREATE INDEX idx__user_follows__user_id ON followers(user_id); 13 + CREATE INDEX idx__user_follows__target_id ON user_follows(target_id); 14 + CREATE INDEX idx__user_follows__user_id ON user_follows(user_id); 15 15 -- +goose StatementEnd 16 16 17 17 -- +goose Down
+3 -3
private/migrations/app/20260112145252_create_mixtapes.sql
··· 8 8 image_url TEXT, 9 9 created_at TEXT DEFAULT CURRENT_TIMESTAMP, 10 10 updated_at TEXT DEFAULT CURRENT_TIMESTAMP, 11 - FOREIGN KEY (user_id) REFERENCES users(id), 11 + FOREIGN KEY (owner_id) REFERENCES users(id) 12 12 ); 13 13 14 - CREATE INDEX idx__mixtapes__owner_id ON mixtapes (owner_id); 15 - CREATE INDEX idx__mixtapes__name ON mixtapes (name); 14 + CREATE INDEX idx__mixtapes__owner_id ON mixtapes(owner_id); 15 + CREATE INDEX idx__mixtapes__name ON mixtapes(name); 16 16 -- +goose StatementEnd 17 17 18 18 -- +goose Down
+3 -3
private/migrations/app/20260113080115_create_artist_followers.sql
··· 4 4 artist_mbid TEXT NOT NULL, 5 5 user_id INTEGER NOT NULL, 6 6 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 - PRIMARY KEY (artist_id, user_id), 8 - FOREIGN KEY (user_id) REFERENCES users(id) 7 + PRIMARY KEY (artist_mbid, user_id), 8 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 9 9 ); 10 10 11 - CREATE INDEX idx__artist_followers__artist_id ON artist_followers (artist_id); 11 + CREATE INDEX idx__artist_followers__artist_mbid ON artist_followers (artist_mbid); 12 12 CREATE INDEX idx__artist_followers__user_id ON artist_followers (user_id); 13 13 -- +goose StatementEnd 14 14
+5 -2
private/migrations/app/20260113080132_create_release_group_likes.sql
··· 5 5 user_id INTEGER NOT NULL, 6 6 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 7 PRIMARY KEY (release_group_mbid, user_id), 8 - FOREIGN KEY (user_id) REFERENCES users (id) 8 + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 9 9 ); 10 + 11 + CREATE INDEX idx__release_group_likes__release_group_mbid ON release_group_likes (release_group_mbid); 12 + CREATE INDEX idx__release_group_likes__user_id ON release_group_likes (user_id); 10 13 -- +goose StatementEnd 11 14 12 15 -- +goose Down 13 16 -- +goose StatementBegin 14 - SELECT 'down SQL query'; 17 + DROP TABLE release_group_likes; 15 18 -- +goose StatementEnd
+4 -3
private/migrations/app/20260113080142_create_recording_likes.sql
··· 2 2 -- +goose StatementBegin 3 3 CREATE TABLE recording_likes ( 4 4 recording_mbid TEXT NOT NULL, 5 - user_id TEXT NOT NULL, 6 - created TEXT WITH TIME ZONE NOT NULL DEFAULT NOW(), 7 - PRIMARY KEY (recording_mbid, user_id) 5 + user_id INTEGER NOT NULL, 6 + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 + PRIMARY KEY (recording_mbid, user_id), 8 + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 8 9 ); 9 10 10 11 CREATE INDEX idx__recording_likes__user_id ON recording_likes (user_id);
+2 -2
private/migrations/app/20260114161552_create_mixtape_recordings.sql
··· 4 4 id INTEGER PRIMARY KEY AUTOINCREMENT, 5 5 mixtape_id INTEGER NOT NULL, 6 6 recording_mbid TEXT NOT NULL, 7 - order INTEGER NOT NULL, 7 + position INTEGER NOT NULL, 8 8 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 9 updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 10 FOREIGN KEY (mixtape_id) REFERENCES mixtapes(id) ··· 12 12 13 13 CREATE INDEX idx__mixtape_recordings__mixtape_id ON mixtape_recordings(mixtape_id); 14 14 CREATE INDEX idx__mixtape_recordings__recording_mbid ON mixtape_recordings(recording_mbid); 15 - CREATE INDEX idx__mixtape_recordings__order_asc ON mixtape_recordings(order ASC); 15 + CREATE INDEX idx__mixtape_recordings__position_asc ON mixtape_recordings(position ASC); 16 16 -- +goose StatementEnd 17 17 18 18 -- +goose Down
+140
scripts/seeds/app.py
··· 1 + import typing 2 + import random 3 + from faker import Faker 4 + import sqlite3 5 + import os 6 + import polars as pl 7 + from musicbrainz import ARTIST_COLUMNS, RELEASE_GROUP_COLUMNS, RECORDING_COLUMNS 8 + 9 + APP_DATABASE_PATH = os.getenv("APP_DATABASE_PATH") or "private/database/app.dev.db" 10 + 11 + def filename(table: str) -> str: 12 + return f"private/data/mbdump-sample/mbdump/{table}" 13 + 14 + 15 + def users(cursor: sqlite3.Cursor, fake: Faker, count: int): 16 + emails = [fake.unique.email() for _ in range(count)] 17 + for index in range(count): 18 + cursor.execute( 19 + "INSERT INTO users (name, email, password) VALUES (?, ?, ?)", 20 + (fake.name(), emails[index], fake.password()) 21 + ) 22 + 23 + 24 + def user_follows(cursor: sqlite3.Cursor, user_count: int, max_follow_count: int): 25 + for user_id in range(1, user_count + 1): 26 + target_ids = random.sample(range(1, user_count + 1), random.randint(1, max_follow_count)) 27 + for target_id in target_ids: 28 + cursor.execute( 29 + "INSERT INTO user_follows (user_id, target_id) VALUES (?, ?)", 30 + (user_id, target_id) 31 + ) 32 + 33 + 34 + def mixtapes(cursor: sqlite3.Cursor, fake: Faker, user_count: int, max_user_amount_count: int) -> list[int]: 35 + ids = [] 36 + for user_id in range(1, user_count + 1): 37 + user_amount_count = random.randint(1, max_user_amount_count) 38 + for _ in range(user_amount_count): 39 + name = fake.sentence(nb_words=3) 40 + description = fake.text(max_nb_chars=40) 41 + mixtape_id = cursor.execute( 42 + "INSERT INTO mixtapes (name, description, owner_id, image_url) VALUES (?, ?, ?, ?) RETURNING id", 43 + (name, description, user_id, fake.image_url()) 44 + ) 45 + mixtape_id = mixtape_id.fetchone()[0] 46 + ids.append(mixtape_id) 47 + return ids 48 + 49 + 50 + def mixtape_followers(cursor: sqlite3.Cursor, mixtape_ids: list[int], user_count: int, max_user_follower_count: int): 51 + for mixtape_id in mixtape_ids: 52 + user_ids = random.sample(range(1, user_count + 1), random.randint(1, max_user_follower_count)) 53 + for user_id in user_ids: 54 + cursor.execute( 55 + "INSERT INTO mixtape_followers (mixtape_id, user_id) VALUES (?, ?)", 56 + (mixtape_id, user_id) 57 + ) 58 + 59 + 60 + def artist_followers(cursor: sqlite3.Cursor, df: pl.DataFrame, user_count: int, max_user_follower_count: int): 61 + for user_id in range(1, user_count + 1): 62 + random_amount = random.randint(1, max_user_follower_count) 63 + artist_mbids = df.sample(n=random_amount, shuffle=True)["gid"].to_list() 64 + for mbid in artist_mbids: 65 + cursor.execute( 66 + "INSERT INTO artist_followers (artist_mbid, user_id) VALUES (?, ?)", 67 + (mbid, user_id) 68 + ) 69 + 70 + 71 + def recording_likes(cursor: sqlite3.Cursor, df: pl.DataFrame, user_count: int, max_user_like_count: int): 72 + for user_id in range(1, user_count + 1): 73 + random_amount = random.randint(1, max_user_like_count) 74 + recording_mbids = df.sample(n=random_amount, shuffle=True)["gid"].to_list() 75 + for mbid in recording_mbids: 76 + cursor.execute( 77 + "INSERT INTO recording_likes (recording_mbid, user_id) VALUES (?, ?)", 78 + (mbid, user_id) 79 + ) 80 + 81 + 82 + def release_group_likes(cursor: sqlite3.Cursor, df: pl.DataFrame, user_count: int, max_user_like_count: int): 83 + for user_id in range(1, user_count + 1): 84 + random_amount = random.randint(1, max_user_like_count) 85 + recording_mbids = df.sample(n=random_amount, shuffle=True)["gid"].to_list() 86 + for mbid in recording_mbids: 87 + cursor.execute( 88 + "INSERT INTO release_group_likes (release_group_mbid, user_id) VALUES (?, ?)", 89 + (mbid, user_id) 90 + ) 91 + 92 + def mixtape_recordings(cursor: sqlite3.Cursor, df: pl.DataFrame, mixtapes_ids: list[int], max_recordings_per_mixtape: int): 93 + for mixtape_id in mixtapes_ids: 94 + random_amount = random.randint(1, max_recordings_per_mixtape) 95 + recording_mbids = df.sample(n=random_amount, shuffle=True)["gid"].to_list() 96 + for index, mbid in enumerate(recording_mbids): 97 + cursor.execute( 98 + "INSERT INTO mixtape_recordings (recording_mbid, mixtape_id, position) VALUES (?, ?, ?)", 99 + (mbid, mixtape_id, index + 1) 100 + ) 101 + 102 + 103 + def read_df(name: str, columns: list[str], schema_overrides: dict[str, typing.Any] = {}) -> pl.DataFrame: 104 + return pl.read_csv( 105 + filename(name), 106 + separator="\t", 107 + has_header=False, 108 + quote_char=None, 109 + null_values=["\\N"], 110 + new_columns=columns, 111 + schema_overrides=schema_overrides, 112 + ) 113 + 114 + 115 + def main(): 116 + conn = sqlite3.connect(APP_DATABASE_PATH) 117 + cursor = conn.cursor() 118 + 119 + fake = Faker() 120 + user_count = 500 121 + 122 + df_artists = read_df("artist", ARTIST_COLUMNS) 123 + df_release_group = read_df("release_group", RELEASE_GROUP_COLUMNS, {"comment": pl.Utf8}) 124 + df_recording = read_df("recording", RECORDING_COLUMNS, {"barcode": pl.Utf8}) 125 + 126 + users(cursor, fake, 500) 127 + user_follows(cursor, 500, 125) 128 + mixtape_ids = mixtapes(cursor, fake, 500, 25) 129 + mixtape_followers(cursor, mixtape_ids, 500, 25) 130 + artist_followers(cursor, df_artists, 500, 100) 131 + recording_likes(cursor, df_recording, 500, 1000) 132 + release_group_likes(cursor, df_release_group, 500, 250) 133 + mixtape_recordings(cursor, df_recording, mixtape_ids, 100) 134 + 135 + conn.commit() 136 + conn.close() 137 + 138 + 139 + if __name__ == "__main__": 140 + main()
+49 -46
scripts/seeds/musicbrainz.py
··· 59 59 return f"private/data/mbdump-sample/mbdump/{table}" 60 60 61 61 62 + ARTIST_COLUMNS = [ 63 + "id", 64 + "gid", 65 + "name", 66 + "sort_name", 67 + "begin_date_year", 68 + "begin_date_month", 69 + "begin_date_day", 70 + "end_date_year", 71 + "end_date_month", 72 + "end_date_day", 73 + "type", 74 + "area", 75 + "gender", 76 + "comment", 77 + "edits_pending", 78 + "last_updated", 79 + "ended", 80 + "begin_area", 81 + "end_area", 82 + ] 83 + 62 84 63 85 def artist(conn: sqlite3.Cursor): 64 - columns = [ 65 - "id", 66 - "gid", 67 - "name", 68 - "sort_name", 69 - "begin_date_year", 70 - "begin_date_month", 71 - "begin_date_day", 72 - "end_date_year", 73 - "end_date_month", 74 - "end_date_day", 75 - "type", 76 - "area", 77 - "gender", 78 - "comment", 79 - "edits_pending", 80 - "last_updated", 81 - "ended", 82 - "begin_area", 83 - "end_area", 84 - ] 85 - 86 - write_to_database(conn, "artist", filename("artist"), columns, None, 1000000) 86 + write_to_database(conn, "artist", filename("artist"), ARTIST_COLUMNS, None, 1000000) 87 87 88 88 89 89 def artist_credit(conn: sqlite3.Cursor): ··· 120 120 ) 121 121 122 122 123 + RELEASE_GROUP_COLUMNS = [ 124 + "id", 125 + "gid", 126 + "name", 127 + "artist_credit", 128 + "type", 129 + "comment", 130 + "edits_pending", 131 + "last_updated", 132 + ] 133 + 123 134 def release_group(conn: sqlite3.Cursor): 124 - columns = [ 125 - "id", 126 - "gid", 127 - "name", 128 - "artist_credit", 129 - "type", 130 - "comment", 131 - "edits_pending", 132 - "last_updated", 133 - ] 134 135 write_to_database( 135 136 conn, 136 137 "release_group", 137 138 filename("release_group"), 138 - columns, 139 + RELEASE_GROUP_COLUMNS, 139 140 {"comment": pl.Utf8}, 140 141 1000000, 141 142 ) ··· 240 241 ) 241 242 242 243 244 + RECORDING_COLUMNS = [ 245 + "id", 246 + "gid", 247 + "name", 248 + "artist_credit", 249 + "length", 250 + "comment", 251 + "edits_pending", 252 + "last_updated", 253 + "video", 254 + ] 255 + 256 + 243 257 def recording(conn: sqlite3.Cursor): 244 - columns = [ 245 - "id", 246 - "gid", 247 - "name", 248 - "artist_credit", 249 - "length", 250 - "comment", 251 - "edits_pending", 252 - "last_updated", 253 - "video", 254 - ] 255 258 write_to_database( 256 - conn, "recording", filename("recording"), columns, {"barcode": pl.Utf8}, 1000000 259 + conn, "recording", filename("recording"), RECORDING_COLUMNS, {"barcode": pl.Utf8}, 1000000 257 260 ) 258 261 259 262
+1 -2
taskfile.yml
··· 46 46 silent: true 47 47 seed.app: 48 48 cmds: 49 - - sqlite3 {{ .APP_DB_PATH }} < scripts/seed_app.sql 50 49 - source scripts/.venv/bin/activate 51 - - python scripts/seed_app.py 50 + - python scripts/seeds/app.py 52 51 reset.app: 53 52 cmds: 54 53 - rm -rf {{ .APP_DB_PATH }}