diff --git a/.gitignore b/.gitignore index ea8ea98..1ae23fe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ reports/* .vscode jackson/* *.png +*.yml +*.sh + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81644bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.11.4-slim-bookworm + +WORKDIR . + +RUN apt-get update && apt-get upgrade -y + +COPY requirements.txt requirements.txt + +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install -r requirements.txt + +COPY . . + +CMD [ "python3", "-u", "app.py" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0d576e --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# About + +This is a front end web display for [this](https://github.com/0x01FE/spotify-artist-time-tracking) other project of mine. + + + + diff --git a/app.py b/app.py index 674820f..9f04eae 100644 --- a/app.py +++ b/app.py @@ -1,93 +1,63 @@ -from flask import Flask, render_template, redirect +from flask import Flask, render_template +import matplotlib +import matplotlib.pyplot as plt +from dateutil.relativedelta import relativedelta +from flask_wtf.csrf import CSRFProtect +from waitress import serve + from datetime import datetime -from datetime import timedelta -import calendar -import json -from os.path import exists -from os import mkdir -from glob import glob -from math import floor from configparser import ConfigParser -import matplotlib.pyplot as plt -from typing import Literal, Optional +import calendar + +import db +import date_range +matplotlib.use("agg") +matplotlib.font_manager.fontManager.addfont("./static/CyberpunkWaifus.ttf") + app = Flask(__name__, static_url_path='', static_folder='static') -if not exists('./reports/'): - mkdir("./reports/") +csrf = CSRFProtect() +csrf.init_app(app) config = ConfigParser() config.read("config.ini") -spotify_times_path = config['PATHES']['spotify_times'] templates_path = config['PATHES']['templates'] +DATABASE = config['PATHES']['DATABASE'] -def msToHour(mili : int) -> int: - return round(mili/1000/60/60, 1) - -def listenTimeFormat(mili : int) -> tuple: - minutes = mili/1000/60 - hours = floor(minutes/60) - return (hours, round(minutes % 60, 2)) -def calculate_total_listening_time(data : dict) -> int: - overall_time = 0 - for artist in data: - overall_time += data[artist]["overall"] +def color_graph(title : str, ax : matplotlib.axes.Axes, plot : matplotlib.lines.Line2D) -> matplotlib.axes.Axes: + # Graph Background + ax.set_facecolor("#00031c") - return overall_time + # Graph Border + for spine in ax.spines.values(): + spine.set_color('xkcd:pink') -# returns path to report -def generate_listening_report(start : datetime, end : datetime) -> str: - if end < start: - print("Error, start date is after end date.") - return + # Data Line Color + for line in plot: + line.set_color("xkcd:hot pink") - days = glob(spotify_times_path + "*.json") + # Axis Label Colors + ax.set_xlabel('Date', color="xkcd:hot pink", font="CyberpunkWaifus", fontsize=15) + ax.set_ylabel("Time (Hours)", color="xkcd:hot pink", font="CyberpunkWaifus", fontsize=15) - days_in_range = [] + # Tick colors + ax.tick_params(axis="x", colors="xkcd:powder blue") + ax.tick_params(axis="y", colors="xkcd:powder blue") - for day in days: - if "overall" in day.split("/")[-1] or "last" in day.split("/")[-1]: - continue + # Title Color + ax.set_title(title, color="xkcd:hot pink", font="CyberpunkWaifus", fontsize=20) - file_date = datetime.strptime(day.split("/")[-1][:10], "%d-%m-%Y") - if file_date >= start and file_date <= end: - days_in_range.append(day) + # Grid + ax.grid(color="xkcd:dark purple") - report = {} - for day in days_in_range: - with open(day, "r") as f: - day_data = json.loads(f.read()) - - for artist in day_data: - if artist not in report: - report[artist.lower()] = { "overall" : 0, "albums" : {} } - - for album in day_data[artist]["albums"]: - if album not in report[artist.lower()]["albums"]: - report[artist.lower()]["albums"][album] = { "overall" : 0, "songs" : {} } - - for song in day_data[artist]["albums"][album]["songs"]: - if song not in report[artist.lower()]["albums"][album]["songs"]: - report[artist.lower()]["albums"][album]["songs"][song] = 0 - - report[artist.lower()]["albums"][album]["songs"][song] += day_data[artist]["albums"][album]["songs"][song] - report[artist.lower()]["overall"] += day_data[artist]["albums"][album]["songs"][song] - report[artist.lower()]["albums"][album]["overall"] += day_data[artist]["albums"][album]["songs"][song] - - if report: - save_location = "reports/" + f'{datetime.strftime(start, "%d-%m-%Y")}-{datetime.strftime(end, "%d-%m-%Y")}.json' - with open(save_location, "w") as f: - f.write(json.dumps(report, indent=4)) - - return save_location - else: - return None + return ax ''' takes in a period of time to use as the X-axis @@ -98,409 +68,244 @@ def generate_listening_report(start : datetime, end : datetime) -> str: Returns path to graph ''' -def generate_overall_graph(period : str) -> str: +def generate_overall_graph(user_id : int, period : str) -> str: # Analyze the past 12 months, including this one so far. if period == 'month': - today = datetime.today() + now = datetime.now() - months = range(1, today.month + 1) - - months_last_year = range(len(months), 12) - - months_with_data = [[], []] # First list is the current year, second is last year - - # Check which of the months in the ranges we have data for - days = glob(spotify_times_path + "*-*-*.json") - - for day in days: - filename = day.split("/")[-1] - - if "overall" in filename or "last" in filename: - continue - - date = filename[:10].split('-') # list with (day, month, year) - - i = 0 - for x in date: - date[i] = int(x) - i+=1 - - if date[2] == today.year and date[1] in months: - if date[1] not in months_with_data[0]: - months_with_data[0].append(date[1]) - - elif date[2] == today.year - 1 and date[1] in months_last_year: - if date[1] not in months_with_data[1]: - months_with_data[1].append(date[1]) - - # last_day = calendar.monthrange(year, month)[1] - monthly_totals = [] + totals = [] dates = [] + for i in range(1, 13): + start = now - relativedelta(months=i) # Why does normal timedelta not support months? + end = now - relativedelta(months=i-1) + time = db.get_total_time(user_id, date_range.DateRange(start, end)) - for month in sorted(months_with_data[1]): - start = datetime.strptime(f"01-{month}-{today.year-1}", "%d-%m-%Y") - end = datetime.strptime(f"{calendar.monthrange(today.year, month)[1]}-{month}-{today.year-1}", "%d-%m-%Y") - - report_path = generate_listening_report(start, end) - - with open(report_path, 'r') as f: - report_data = json.loads(f.read()) + if not time: + break - monthly_totals.append(msToHour(calculate_total_listening_time(report_data))) - dates.append(f'{month}-{today.year-1}') + totals.append(time.to_hours()) + dates.append(start.strftime("%Y-%m-%d")) - for month in sorted(months_with_data[0]): - start = datetime.strptime(f"01-{month}-{today.year}", "%d-%m-%Y") - end = datetime.strptime(f"{calendar.monthrange(today.year, month)[1]}-{month}-{today.year}", "%d-%m-%Y") + totals = list(reversed(totals)) + dates = list(reversed(dates)) - report_path = generate_listening_report(start, end) + fig, ax = plt.subplots(facecolor="xkcd:black") + line = ax.plot(dates, totals) - with open(report_path, 'r') as f: - report_data = json.loads(f.read()) + color_graph("Monthly Summary", ax, line) - monthly_totals.append(msToHour(calculate_total_listening_time(report_data))) - dates.append(f'{month}-{today.year}') - - fig, ax = plt.subplots() - ax.plot(dates, monthly_totals) - ax.set(xlabel='Date', ylabel='time (hours)', title='Listening Time') - ax.grid() - - for i, txt in enumerate(monthly_totals): - ax.annotate(txt, (dates[i], monthly_totals[i])) + for i, txt in enumerate(totals): + ax.annotate(txt, (dates[i], totals[i]), color="xkcd:powder blue") fig.savefig("static/month.png") - return "static/month.png" # Graph of the past eight weeks elif period == 'week': - today = datetime.today() - - weeks = [] - end = today - for week in range(0, 9): - start = end - timedelta(days=7) - weeks.append((start, end)) - end = start - timedelta(days=1) + now = datetime.now() - weekly_totals = [] + totals = [] dates = [] - for week in reversed(weeks): - report_path = generate_listening_report(week[0], week[1]) - if not report_path: - continue + for i in range(1, 9): + start = now - relativedelta(weeks=i) # Why does normal timedelta not support months? + end = now - relativedelta(weeks=i-1) + time = db.get_total_time(user_id, date_range.DateRange(start, end)) - with open(report_path, 'r') as f: - report_data = json.loads(f.read()) + if not time: + break - weekly_totals.append(msToHour(calculate_total_listening_time(report_data))) - dates.append(f'{week[0].day}-{week[0].month}') + totals.append(time.to_hours()) + dates.append(start.strftime("%Y-%m-%d")) - fig, ax = plt.subplots() - ax.plot(dates, weekly_totals) - ax.set(xlabel='Date', ylabel='time (hours)', title='Listening Time') - ax.grid() + totals = list(reversed(totals)) + dates = list(reversed(dates)) - for i, txt in enumerate(weekly_totals): - ax.annotate(txt, (dates[i], weekly_totals[i])) + fig, ax = plt.subplots(facecolor="xkcd:black") + line = ax.plot(dates, totals) + + color_graph("Weekly Summary", ax, line) + + for i, txt in enumerate(totals): + ax.annotate(txt, (dates[i], totals[i]), color="xkcd:powder blue") fig.savefig("static/week.png", bbox_inches='tight') - return "static/week.png" # Graph of the past 14 days elif period == 'day': - today = datetime.today() - timedelta(days=3) + now = datetime.strptime("2023-09-16", "%Y-%m-%d") - daily_totals = [] + totals = [] dates = [] - for day in range(0, 15): - with open(spotify_times_path + f"{datetime.strftime(today, '%d-%m-%Y')}.json", 'r') as f: - report_data = json.loads(f.read()) + for i in range(1, 9): + start = now - relativedelta(days=i) # Why does normal timedelta not support months? + end = now - relativedelta(days=i-1) + time = db.get_total_time(user_id, date_range.DateRange(start, end)) - daily_totals.append(msToHour(calculate_total_listening_time(report_data))) - dates.append(f'{today.day}') + if not time: + time = 0 - today = today - timedelta(days=1) + totals.append(time.to_hours()) + dates.append(start.strftime("%m-%d")) - dates = [a for a in reversed(dates)] - daily_totals = [a for a in reversed(daily_totals)] + totals = list(reversed(totals)) + dates = list(reversed(dates)) - fig, ax = plt.subplots() - ax.plot(dates, daily_totals) - ax.set(xlabel='Date', ylabel='time (hours)') - ax.grid() + fig, ax = plt.subplots(facecolor="xkcd:black") + line = ax.plot(dates, totals) - for i, txt in enumerate(daily_totals): - ax.annotate(txt, (dates[i], daily_totals[i])) + color_graph("Daily Summary", ax, line) - fig.savefig("static/day.png", bbox_inches='tight') - return "static/day.png" + for i, txt in enumerate(totals): + ax.annotate(txt, (dates[i], totals[i]), color="xkcd:powder blue") + fig.savefig("static/day.png", bbox_inches='tight') else: return 'bad period' + plt.close() + return f'static/{period}.png' -def keyfunc(tup : tuple) -> int: - key, d = tup - return d['overall'] - -def get_top(item_type : Literal['artists', 'albums', 'songs'], top : Optional[int] = None) -> dict: - with open(spotify_times_path + "overall.json", 'r') as f: - data = json.loads(f.read()) - - if item_type == 'artists': - data = sorted(data.items(), key=keyfunc, reverse=True) - if top: - data = data[:top] - - sorted_artists = {} - for artist_tuple in data: - artist_name, artist_info = artist_tuple - - sorted_artists[artist_name.replace("-", " ").title()] = (listenTimeFormat(artist_info["overall"]), artist_name) - - return sorted_artists - - elif item_type == 'albums': - albums = {} - for artist in data: - for album in data[artist]["albums"]: - albums[album] = { - "overall" : data[artist]["albums"][album]["overall"], - "artist" : artist - } - data = sorted(albums.items(), key=keyfunc, reverse=True) - if top: - data = data[:top] - - sorted_albums = {} - for album in data: - album_title, album_info = album - - sorted_albums[album_title] = (listenTimeFormat(album_info["overall"]), album_info["artist"].replace("-", " ").title(), album_info['artist']) - - return sorted_albums - - elif item_type == 'songs': - songs = {} - for artist in data: - for album in data[artist]["albums"]: - for song in data[artist]["albums"][album]["songs"]: - songs[song] = { - "overall" : data[artist]["albums"][album]["songs"][song], - "artist" : artist - } - data = sorted(songs.items(), key=keyfunc, reverse=True) - if top: - data = data[:top] - - sorted_songs = {} - for song in data: - song_title, song_info = song - - sorted_songs[song_title] = (listenTimeFormat(song_info["overall"]), song_info["artist"].replace("-", " ").title(), song_info["artist"]) - - return sorted_songs -@app.route('/') -def root(): - for period in ['day', 'week', 'month']: - generate_overall_graph(period) +@app.route('//') +def overview(user : int): - today = datetime.today() + for period in ['day', 'week', 'month']: + generate_overall_graph(user, period) - top_artists = get_top('artists', top=5) - top_albums = get_top('albums', top=5) - top_songs = get_top('songs', top=5) - with open(spotify_times_path + "overall.json", 'r') as f: - data = json.loads(f.read()) + today = datetime.today() - total_time = listenTimeFormat(calculate_total_listening_time(data)) - artist_count = len(data) + top_artists = db.get_top_artists(user, top=5) + top_albums = db.get_top_albums(user, top=5) + top_songs = db.get_top_songs(user, top=5) - # For total albums/songs count - all_albums = [] - all_songs = [] - for artist in data: - for album in data[artist]["albums"]: - if album not in all_albums: - all_albums.append(album) - for song in data[artist]["albums"][album]["songs"]: - if song not in all_songs: - all_songs.append(song) + total_time = db.get_total_time(user).to_hour_and_seconds() - return render_template('home.html', top_albums=top_albums, top_songs=top_songs, top_artists=top_artists, year=today.year, month=today.month, artist_count=artist_count, total_time=total_time, album_count=len(all_albums), song_count=len(all_songs)) + artist_count = db.get_artist_count(user) + album_count = db.get_album_count(user) + song_count = db.get_song_count(user) -@app.route('/overall/') -def overall(): - return redirect('/') -@app.route('/overall/artists/') -def overall_artists(): + return render_template('overall.html', top_albums=top_albums, top_songs=top_songs, top_artists=top_artists, year=today.year, month=today.month, artist_count=artist_count, total_time=total_time, album_count=album_count, song_count=song_count) - top_artists = get_top('artists') +@app.route('//artists/') +def artists_overview(user: int): - return render_template('overall_artists.html', data=top_artists) + top_artists = db.get_top_artists(user) -@app.route('/overall/albums/') -def overall_albums(): - top_albums = get_top('albums') + return render_template('artists_overview.html', data=top_artists) - return render_template('overall_albums.html', top_albums=top_albums) +@app.route('//artists//') +def artist_overview(user: int, artist : str): + artist = artist.lower() -@app.route('/overall/songs/') -def overall_songs(): - top_songs = get_top('songs') + total_time = db.get_artist_total(user, artist).to_hour_and_seconds() + top_albums = db.get_artist_top_albums(user, artist) + top_songs = db.get_artist_top_songs(user, artist) - return render_template('overall_songs.html', top_songs=top_songs) + return render_template('artist.html', artist_name=artist.replace('-', ' ').title(), artist_listen_time=total_time, top_albums=top_albums, top_songs=top_songs) -@app.route('/month///') -def overall_month(year : str, month : str): - month = int(month) - year = int(year) +@app.route('//albums/') +def albums_overview(user : int): + top_albums = db.get_top_albums(user) - today = datetime.now() + return render_template('albums_overview.html', top_albums=top_albums) - if month > today.month or month < 1: - return 'Month is invalid' +@app.route('//songs/') +def songs_overview(user : int): + top_songs = db.get_top_songs(user) - last_day = calendar.monthrange(year, month)[1] + return render_template('songs_overview.html', top_songs=top_songs) - if month < 10: - start = datetime.strptime(f"01-0{month}-{year}", "%d-%m-%Y") - else: - start = datetime.strptime(f"01-{month}-{year}", "%d-%m-%Y") - if month == today.month: - end = today - else: - end = datetime.strptime(f"{last_day}-{month}-{year}", "%d-%m-%Y") - filename = f'{datetime.strftime(start, "%d-%m-%Y")}-{datetime.strftime(end, "%d-%m-%Y")}.json' +@app.route('//month///') +def month_overview(user : int, year : int, month : int): - if not exists("reports/" + filename) or month == today.month: - generate_listening_report(start, end) + period = date_range.DateRange() - with open("reports/" + filename, 'r') as f: - data = json.loads(f.read()) + if not period.get_range(year, month): + return "Invalid month or year." - total_time = listenTimeFormat(calculate_total_listening_time(data)) - artist_count = len(data) + top_artists = db.get_top_artists(user, period, top=5) + top_albums = db.get_top_albums(user, period, top=5) + top_songs = db.get_top_songs(user, period, top=5) - data = sorted(data.items(), key=keyfunc, reverse=True) + if not (total_time := db.get_total_time(user, period)): + return "No data for this month." + total_time = total_time.to_hour_and_seconds() - sorted_artists = {} - for artist_tuple in data: - artist_name, artist_info = artist_tuple + artist_count = db.get_artist_count(user, period) + album_count = db.get_album_count(user, period) + song_count = db.get_song_count(user, period) - sorted_artists[artist_name.replace("-", " ").title()] = (listenTimeFormat(artist_info["overall"]), artist_name) links = (f"../../{year}/{month - 1}", f"../../{year}/{month + 1}") - return render_template('overall_month.html', data=sorted_artists, month_name=calendar.month_name[month], year=year, selector_links=links, month_total=total_time, artist_total=artist_count) - + return render_template('month_overview.html', month_name=calendar.month_name[month], year=year, top_artists=top_artists, top_albums=top_albums, top_songs=top_songs, artist_count=artist_count, total_time=total_time, album_count=album_count, song_count=song_count) -@app.route('/month////') -def artist_month(year : str, month : str, artist : str): +@app.route('//month///artists//') +def artist_month_overview(user : int, year : int, month : int, artist : str): artist = artist.lower() - month = int(month) - year = int(year) - today = datetime.now() + period = date_range.DateRange() - if month > today.month or month < 1: - return 'Month is invalid' + if not period.get_range(year, month): + return "Invalid month or year." - last_day = calendar.monthrange(year, month)[1] + total_time = db.get_artist_total(user, artist, period).to_hour_and_seconds() + top_albums = db.get_artist_top_albums(user, artist, period) + top_songs = db.get_artist_top_songs(user, artist, period) - if month < 10: - start = datetime.strptime(f"01-0{month}-{year}", "%d-%m-%Y") - else: - start = datetime.strptime(f"01-{month}-{year}", "%d-%m-%Y") - - if month == today.month: - end = today - else: - end = datetime.strptime(f"{last_day}-{month}-{year}", "%d-%m-%Y") - - filename = f'{datetime.strftime(start, "%d-%m-%Y")}-{datetime.strftime(end, "%d-%m-%Y")}.json' + return render_template('artist_month_overview.html', artist_name=artist.replace('-', ' ').title(), month_name=calendar.month_name[month], year=year, artist_listen_time=total_time, top_albums=top_albums, top_songs=top_songs) - if not exists("reports/" + filename) or month == today.month: - generate_listening_report(start, end) +@app.route('//month///artists/') +def artists_month_overview(user : int, year : int, month : int): - with open("reports/" + filename, 'r') as f: - data = json.loads(f.read()) + period = date_range.DateRange() - total_time = listenTimeFormat(data[artist]["overall"]) + if not period.get_range(year, month): + return "Invalid month or year." - # Sort albums by listen time - albums_sorted = sorted(data[artist]["albums"].items(), key=keyfunc, reverse=True) + top_artists = db.get_top_artists(user, period) - top_albums = {} - for album_tuple in albums_sorted: - album_name, album_info = album_tuple + return render_template('artists_month_overview.html', top_artists=top_artists, month_name=calendar.month_name[month], year=year) - top_albums[album_name] = listenTimeFormat(album_info["overall"]) +@app.route('//month///albums/') +def albums_month_overview(user : int, year : int, month : int): + period = date_range.DateRange() - # Make dict of top songs - all_songs = {} - for album in data[artist]["albums"]: - for song in data[artist]["albums"][album]["songs"]: - all_songs[song] = data[artist]["albums"][album]["songs"][song] + if not period.get_range(year, month): + return "Invalid month or year." - sorted_songs = {k: v for k, v in sorted(all_songs.items(), key=lambda item: item[1], reverse=True)} + top_albums = db.get_top_albums(user, period) - top_songs = {} - for song in sorted_songs: - top_songs[song] = listenTimeFormat(sorted_songs[song]) + return render_template('albums_month_overview.html', top_albums=top_albums, month_name=calendar.month_name[month], year=year) - return render_template('artist_month.html', artist_name=artist.replace('-', ' ').title(), month_name=calendar.month_name[month], year=year, artist_listen_time=total_time, top_albums=top_albums, top_songs=top_songs) +@app.route('//month///songs/') +def songs_month_overview(user : int, year : int, month : int): + period = date_range.DateRange() + if not period.get_range(year, month): + return "Invalid month or year." + top_songs = db.get_top_songs(user, period) -@app.route('/overall//') -def artist(artist : str): - artist = artist.lower() - - - with open(spotify_times_path + "overall.json", 'r') as f: - data = json.loads(f.read()) - - total_time = listenTimeFormat(data[artist]["overall"]) + return render_template('songs_month_overview.html', top_songs=top_songs, month_name=calendar.month_name[month], year=year) - # Sort albums by listen time - albums_sorted = sorted(data[artist]["albums"].items(), key=keyfunc, reverse=True) - top_albums = {} - for album_tuple in albums_sorted: - album_name, album_info = album_tuple - top_albums[album_name] = listenTimeFormat(album_info["overall"]) - - - # Make dict of top songs - all_songs = {} - for album in data[artist]["albums"]: - for song in data[artist]["albums"][album]["songs"]: - all_songs[song] = data[artist]["albums"][album]["songs"][song] - - sorted_songs = {k: v for k, v in sorted(all_songs.items(), key=lambda item: item[1], reverse=True)} - - top_songs = {} - for song in sorted_songs: - top_songs[song] = listenTimeFormat(sorted_songs[song]) +@app.route('/') +def root(): + return 'home' - return render_template('artist.html', artist_name=artist.replace('-', ' ').title(), artist_listen_time=total_time, top_albums=top_albums, top_songs=top_songs) -@app.route('/test/') -def test(): - return generate_overall_graph("day") +if __name__ == '__main__': + serve(app, host='0.0.0.0', port=802) diff --git a/date_range.py b/date_range.py new file mode 100644 index 0000000..bf82a15 --- /dev/null +++ b/date_range.py @@ -0,0 +1,33 @@ +import datetime +import calendar + +class DateRange: + + start : datetime.datetime + end : datetime.datetime + + def __init__(self, start : datetime.datetime | None = None, end : datetime.datetime | None = None): + self.start = start + self.end = end + + def to_str(self) -> tuple[str, str]: + return (self.start.strftime("%Y-%m-%d"), self.end.strftime("%Y-%m-%d")) + + def get_range(self, year : int, month : int) -> bool: + + today = datetime.datetime.today() + + if month > today.month or month < 1 or year > today.year or year < 2023: + return False + + + last_day = calendar.monthrange(year, month)[1] + + if month < 10: + self.end = datetime.datetime.strptime(f"{year}-0{month}-{last_day}", "%Y-%m-%d") + else: + self.end = datetime.datetime.strptime(f"{year}-{month}-{last_day}", "%Y-%m-%d") + + self.start = datetime.datetime.strptime(f"{year}-{month}", "%Y-%m") + return True + diff --git a/db.py b/db.py new file mode 100644 index 0000000..56743ce --- /dev/null +++ b/db.py @@ -0,0 +1,239 @@ +import sqlite3 as sql +from configparser import ConfigParser +from typing import Optional +from collections import OrderedDict + + +import listen_time +import date_range + +config = ConfigParser() +config.read("config.ini") + + +DATABASE = config['PATHES']['DATABASE'] + + + +class Opener(): + def __init__(self): + self.con = sql.connect(DATABASE) + + def __enter__(self): + return self.con, self.con.cursor() + + def __exit__(self, type, value, traceback): + self.con.commit() + self.con.close() + + + + +def get_top_artists(user_id : int, range : date_range.DateRange | None = None, top : Optional[int] = None) -> dict: + dated = False + if range: + dated = True + start, end = range.to_str() + + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT artist_name, SUM(total_time) FROM (SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY dated.song, songs.artist) GROUP BY artist_name ORDER BY SUM(total_time) DESC", [user_id, start, end]) + else: + cur.execute("SELECT artist_name, SUM(total_time) FROM (SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id WHERE dated.user = ? GROUP BY dated.song, songs.artist) GROUP BY artist_name ORDER BY SUM(total_time) DESC", [user_id]) + results = cur.fetchall() + + if top: + results = results[:top] + + # Format results + top = OrderedDict() + for artist in results: + top[artist[0].replace('-', ' ').title()] = (listen_time.ListenTime(artist[1]).to_hour_and_seconds(), artist[0]) + + return top + + + +def get_top_albums(user_id : int, range : date_range.DateRange | None = None, top : Optional[int] = None) -> dict: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT artist_name, album_name, SUM(total_time) total_time FROM (SELECT artists.name artist_name, albums.name album_name, songs.name song_name, COUNT(songs.name) * songs.length total_time FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY songs.name) GROUP BY album_name ORDER BY total_time DESC", [user_id, start, end]) + else: + cur.execute("SELECT artist_name, album_name, SUM(total_time) total_time FROM (SELECT artists.name artist_name, albums.name album_name, songs.name song_name, COUNT(songs.name) * songs.length total_time FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? GROUP BY songs.name) GROUP BY album_name ORDER BY total_time DESC", [user_id]) + results = cur.fetchall() + + if top: + results = results[:top] + + # Format results + top = OrderedDict() + for album in results: + top[album[1]] = (listen_time.ListenTime(album[2]).to_hour_and_seconds(), album[0].replace('-', ' ').title(), album[0]) + + return top + + + +def get_top_songs(user_id : int, range : date_range.DateRange | None = None, top : Optional[int] = None) -> dict: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY dated.song, songs.artist ORDER BY total_time DESC", [user_id, start, end]) + else: + cur.execute("SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? GROUP BY dated.song, songs.artist ORDER BY total_time DESC", [user_id]) + results = cur.fetchall() + + if top: + results = results[:top] + + # Format results + top = OrderedDict() + for song in results: + top[song[1]] = (listen_time.ListenTime(song[2]).to_hour_and_seconds(), song[0].replace('-', ' ').title(), song[0]) + + return top + + + +def get_total_time(user_id : int, range : date_range.DateRange | None = None) -> listen_time.ListenTime | None: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT SUM(total_time) total_time FROM (SELECT artists.name artist_name, songs.name song_name, dated.song song_id, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY dated.song, songs.artist)", [user_id, start, end]) + else: + cur.execute("SELECT SUM(total_time) total_time FROM (SELECT artists.name artist_name, songs.name song_name, dated.song song_id, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? GROUP BY dated.song, songs.artist)", [user_id]) + results = cur.fetchall() + + if results[0][0]: + return listen_time.ListenTime(results[0][0]) + else: + return None + + + +def get_artist_count(user_id : int, range : date_range.DateRange | None = None) -> int: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT COUNT(DISTINCT songs.artist) artist_count FROM dated INNER JOIN songs ON dated.song=songs.id WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ?", [user_id, start, end]) + else: + cur.execute("SELECT COUNT(DISTINCT songs.artist) artist_count FROM dated INNER JOIN songs ON dated.song=songs.id WHERE dated.user = ?", [user_id]) + results = cur.fetchall() + + return results[0][0] + + + +def get_album_count(user_id : int, range : date_range.DateRange | None = None) -> int: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT COUNT(DISTINCT dated.song) song_count FROM dated WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ?", [user_id, start, end]) + else: + cur.execute("SELECT COUNT(DISTINCT dated.song) song_count FROM dated WHERE dated.user = ?", [user_id]) + results = cur.fetchall() + + return results[0][0] + + + +def get_song_count(user_id : int, range : date_range.DateRange | None = None) -> int: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT COUNT(DISTINCT dated.song) song_count FROM dated WHERE dated.user = ? AND DATE(dated.date) BETWEEN ? AND ?", [user_id, start, end]) + else: + cur.execute("SELECT COUNT(DISTINCT dated.song) song_count FROM dated WHERE dated.user = ?", [user_id]) + results = cur.fetchall() + + return results[0][0] + + + +def get_artist_top_albums(user_id : int, artist : str, range : date_range.DateRange | None = None) -> dict: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT artist_name, album_name, SUM(total_time) total_time FROM (SELECT artists.name artist_name, albums.name album_name, songs.name song_name, COUNT(songs.name) * songs.length total_time FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND artists.name = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY songs.name) GROUP BY album_name ORDER BY total_time DESC", [user_id, artist, start, end]) + else: + cur.execute("SELECT artist_name, album_name, SUM(total_time) total_time FROM (SELECT artists.name artist_name, albums.name album_name, songs.name song_name, COUNT(songs.name) * songs.length total_time FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND artists.name = ? GROUP BY songs.name) GROUP BY album_name ORDER BY total_time DESC", [user_id, artist]) + results = cur.fetchall() + + # Format results + top = OrderedDict() + for album in results: + top[album[1]] = listen_time.ListenTime(album[2]).to_hour_and_seconds() + + return top + + +def get_artist_top_songs(user_id : int, artist : str, range : date_range.DateRange | None = None) -> dict: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND artists.name = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY dated.song, songs.artist ORDER BY total_time DESC", [user_id, artist, start, end]) + else: + cur.execute("SELECT artists.name artist_name, songs.name song_name, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id INNER JOIN albums on songs.album=albums.id WHERE dated.user = ? AND artists.name = ? GROUP BY dated.song, songs.artist ORDER BY total_time DESC", [user_id, artist]) + results = cur.fetchall() + + # Format results + top = OrderedDict() + for song in results: + top[song[1]] = listen_time.ListenTime(song[2]).to_hour_and_seconds() + + return top + + +def get_artist_total(user_id : int, artist : str, range : date_range.DateRange | None = None) -> listen_time.ListenTime | None: + dated = False + if range: + dated = True + start, end = range.to_str() + + with Opener() as (con, cur): + if dated: + cur.execute("SELECT SUM(total_time) total_time FROM (SELECT artists.name artist_name, songs.name song_name, dated.song song_id, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id WHERE dated.user = ? AND artists.name = ? AND DATE(dated.date) BETWEEN ? AND ? GROUP BY dated.song, songs.artist)", [user_id, artist, start, end]) + else: + cur.execute("SELECT SUM(total_time) total_time FROM (SELECT artists.name artist_name, songs.name song_name, dated.song song_id, songs.length * COUNT(songs.name) total_time, COUNT(songs.name) cnt FROM dated INNER JOIN songs ON dated.song=songs.id INNER JOIN artists ON songs.artist=artists.id WHERE dated.user = ? AND artists.name = ? GROUP BY dated.song, songs.artist)", [user_id, artist]) + results = cur.fetchall() + + if results[0][0]: + return listen_time.ListenTime(results[0][0]) + else: + return None + + diff --git a/listen_time.py b/listen_time.py new file mode 100644 index 0000000..5758cd3 --- /dev/null +++ b/listen_time.py @@ -0,0 +1,18 @@ +import math + +class ListenTime: + + mili : int + + def __init__(self, mili : int): + self.mili = mili + + def to_hour_and_seconds(self) -> tuple[int, int]: + minutes = self.mili/1000/60 + hours = math.floor(minutes/60) + + return (hours, round((minutes % 60), 2)) + + def to_hours(self) -> int: + return round(self.mili/1000/60/60, 1) + diff --git a/requirements.txt b/requirements.txt index fa157fe..f1a924f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ -matplotlib==3.7.2 +matplotlib==3.8.0 numpy==1.25.2 Flask==2.2.3 +waitress==2.1.2 +Werkzeug==2.2.3 +Flask-WTF==1.1.1 diff --git a/static/CyberpunkWaifus.ttf b/static/CyberpunkWaifus.ttf new file mode 100644 index 0000000..230fdcf Binary files /dev/null and b/static/CyberpunkWaifus.ttf differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..e0e7214 Binary files /dev/null and b/static/favicon.ico differ diff --git a/templates/albums_month_overview.html b/templates/albums_month_overview.html new file mode 100644 index 0000000..780a3ea --- /dev/null +++ b/templates/albums_month_overview.html @@ -0,0 +1,18 @@ + + +
+ + overall albums + +
+ +
{{ month_name}} {{ year }}
+ << back +
+
Top Albums
+ {% for album in top_albums %} +

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

+ {% endfor %} +
+ + \ No newline at end of file diff --git a/templates/overall_albums.html b/templates/albums_overview.html similarity index 58% rename from templates/overall_albums.html rename to templates/albums_overview.html index 289c650..9e21a00 100644 --- a/templates/overall_albums.html +++ b/templates/albums_overview.html @@ -1,6 +1,7 @@
+ overall albums
@@ -9,7 +10,7 @@
Top Albums
{% for album in top_albums %} -

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

+

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

{% endfor %}
diff --git a/templates/artist.html b/templates/artist.html index 6eee5dd..0158393 100644 --- a/templates/artist.html +++ b/templates/artist.html @@ -1,6 +1,7 @@
+ {{ artist_name }} overall
@@ -21,7 +22,7 @@ {% endfor %} - +
Top Songs
diff --git a/templates/artist_month.html b/templates/artist_month_overview.html similarity index 93% rename from templates/artist_month.html rename to templates/artist_month_overview.html index 3ed698b..ca5fc4d 100644 --- a/templates/artist_month.html +++ b/templates/artist_month_overview.html @@ -1,6 +1,7 @@
+
@@ -21,7 +22,7 @@ {% endfor %}
- +
Top Songs
diff --git a/templates/artists_month_overview.html b/templates/artists_month_overview.html new file mode 100644 index 0000000..0168244 --- /dev/null +++ b/templates/artists_month_overview.html @@ -0,0 +1,18 @@ + + +
+ + overall + +
+ +
{{ month_name}} {{ year }}
+ << back +
+
Top Artists
+ {% for artist in top_artists %} +

{{ loop.index }}. {{ artist }} : {{ top_artists[artist][0][0] }} hours {{ top_artists[artist][0][1] }} minutes

+ {% endfor %} +
+ + diff --git a/templates/overall_artists.html b/templates/artists_overview.html similarity index 61% rename from templates/overall_artists.html rename to templates/artists_overview.html index 054627f..20e7079 100644 --- a/templates/overall_artists.html +++ b/templates/artists_overview.html @@ -1,6 +1,7 @@
+ overall
@@ -9,8 +10,8 @@
Top Artists
{% for artist in data %} -

{{ loop.index }}. {{ artist }} : {{ data[artist][0][0] }} hours {{ data[artist][0][1] }} minutes

+

{{ loop.index }}. {{ artist }} : {{ data[artist][0][0] }} hours {{ data[artist][0][1] }} minutes

{% endfor %}
- \ No newline at end of file + diff --git a/templates/month_overview.html b/templates/month_overview.html new file mode 100644 index 0000000..912d566 --- /dev/null +++ b/templates/month_overview.html @@ -0,0 +1,52 @@ + + +
+ + + {{ month_name }} {{ year }} Overview +
+ +

{{ month_name }} {{ year }} Overview

+
Total Listening Time: {{ total_time[0] }} hours {{ total_time[1] }} minutes
+
Total Artists: {{ artist_count }}
+
Total Albums: {{ album_count }}
+
Total Songs: {{ song_count }}
+ + +
+
+
Top Artists
+ {% for artist in top_artists %} +

{{ loop.index }}. {{ artist }} : {{ top_artists[artist][0][0] }} hours {{ top_artists[artist][0][1] }} minutes

+ {% endfor %} +
+
+
Top Albums
+ {% for album in top_albums %} +

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

+ {% endfor %} +
+
+
Top Songs
+ {% for song in top_songs %} +

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

+ {% endfor %} +
+
+ + + + \ No newline at end of file diff --git a/templates/home.html b/templates/overall.html similarity index 54% rename from templates/home.html rename to templates/overall.html index 7416084..f2bbcb9 100644 --- a/templates/home.html +++ b/templates/overall.html @@ -1,12 +1,13 @@
+ Overview
Music Stat Display Home
- +
Total Listening Time: {{ total_time[0] }} hours {{ total_time[1] }} minutes
Total Artists: {{ artist_count }}
Total Albums: {{ album_count }}
@@ -15,36 +16,33 @@
-
Top Artists
+
Top Artists
{% for artist in top_artists %} -

{{ loop.index }}. {{ artist }} : {{ top_artists[artist][0][0] }} hours {{ top_artists[artist][0][1] }} minutes

+

{{ loop.index }}. {{ artist }} : {{ top_artists[artist][0][0] }} hours {{ top_artists[artist][0][1] }} minutes

{% endfor %}
-
Top Albums
+
Top Albums
{% for album in top_albums %} -

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

+

{{ loop.index }}. {{ album }} by {{ top_albums[album][1] }}: {{ top_albums[album][0][0] }} hours {{ top_albums[album][0][1] }} minutes

{% endfor %}
-
Top Songs
+
Top Songs
{% for song in top_songs %} -

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

+

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

{% endfor %}
-
Daily Summary
-
Weekly Summary
-
Monthly Summary
diff --git a/templates/overall_month.html b/templates/overall_month.html index b1a3121..41a0bb9 100644 --- a/templates/overall_month.html +++ b/templates/overall_month.html @@ -1,6 +1,7 @@
+ {{ month_name }} {{ year }} overview
diff --git a/templates/songs_month_overview.html b/templates/songs_month_overview.html new file mode 100644 index 0000000..4b19137 --- /dev/null +++ b/templates/songs_month_overview.html @@ -0,0 +1,18 @@ + + +
+ + overall albums + +
+ +
{{ month_name}} {{ year }}
+ << back +
+
Top Songs
+ {% for song in top_songs %} +

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

+ {% endfor %} +
+ + \ No newline at end of file diff --git a/templates/overall_songs.html b/templates/songs_overview.html similarity index 67% rename from templates/overall_songs.html rename to templates/songs_overview.html index 27276c8..0114c37 100644 --- a/templates/overall_songs.html +++ b/templates/songs_overview.html @@ -1,6 +1,7 @@
+ overall albums
@@ -9,7 +10,7 @@
Top Songs
{% for song in top_songs %} -

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

+

{{ loop.index }}. {{ song }} by {{ top_songs[song][1] }}: {{ top_songs[song][0][0] }} hours {{ top_songs[song][0][1] }} minutes

{% endfor %}