251 lines
8.2 KiB
Python
251 lines
8.2 KiB
Python
import os
|
|
import sqlite3
|
|
import datetime
|
|
import flask
|
|
import re
|
|
|
|
def is_youtube_id(video_id):
|
|
"""Check if the video_id is a valid YouTube ID"""
|
|
return re.match(r'^[a-zA-Z0-9_-]{11}$', str(video_id)) is not None
|
|
|
|
def read_music_database():
|
|
conn = sqlite3.connect("assets/localwrapped/music.db")
|
|
c = conn.cursor()
|
|
c.execute('SELECT * FROM music')
|
|
music_list = c.fetchall()
|
|
conn.close()
|
|
return music_list
|
|
|
|
def read_count_database(year: int):
|
|
db_path = f'assets/localwrapped/count-{year}.db' if year != datetime.datetime.now().year else 'assets/localwrapped/count.db'
|
|
if not os.path.exists(db_path):
|
|
return []
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
c = conn.cursor()
|
|
c.execute('SELECT * FROM count')
|
|
count_list = c.fetchall()
|
|
conn.close()
|
|
return count_list
|
|
|
|
def get_available_years():
|
|
years = []
|
|
for file in os.listdir('assets/localwrapped'):
|
|
if file.startswith('count') and file.endswith('.db'):
|
|
year = file.split('-')[-1].split('.')[0]
|
|
if year.isdigit():
|
|
years.append(int(year))
|
|
return sorted(years, reverse=True)
|
|
|
|
def total_play_time(year: int):
|
|
total_time = 0
|
|
music_data = {row[0]: row for row in read_music_database()}
|
|
|
|
for count_entry in read_count_database(year):
|
|
if count_entry[0] in music_data:
|
|
total_time += float(music_data[count_entry[0]][7]) * count_entry[1]
|
|
|
|
return total_time
|
|
|
|
def generatehtmlcode(year: int = datetime.datetime.now().year):
|
|
available_years = get_available_years()
|
|
current_year = datetime.datetime.now().year
|
|
selected_year = year if year in available_years else current_year
|
|
|
|
html = '''<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Halhadus' Local Wrapped</title>
|
|
<link rel="icon" type="image/png" href="assets/favicon.png">
|
|
<meta name="description" content="Halhadus' Local Wrapped">
|
|
<meta name="author" content="Halhadus">
|
|
<meta property="og:title" content="Halhadus' Local Wrapped">
|
|
<meta property="og:description" content="Halhadus' Local Wrapped">
|
|
<meta property="og:url" content="https://halhadus.rocks/localwrapped.html">
|
|
<style>
|
|
:root {
|
|
--bg-color: #1f1f1f;
|
|
--card-bg: #2a2a2a;
|
|
--border-color: #333;
|
|
--text-color: #ffffff;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.year-selector {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin: 1rem 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.year-button {
|
|
background: #333;
|
|
border: 1px solid #444;
|
|
color: var(--text-color);
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 5px;
|
|
text-decoration: none;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.year-button.active {
|
|
background: #444;
|
|
border-color: #555;
|
|
}
|
|
|
|
.music-card {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
margin: 1rem 0;
|
|
padding: 1rem;
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.thumbnail {
|
|
width: 100%;
|
|
border-radius: 6px;
|
|
aspect-ratio: 16/9;
|
|
object-fit: cover;
|
|
border: 2px solid var(--border-color);
|
|
}
|
|
|
|
.play-button {
|
|
background: #333;
|
|
border: 1px solid #444;
|
|
color: var(--text-color);
|
|
padding: 0.5rem 1rem;
|
|
margin: 0.3rem;
|
|
border-radius: 5px;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
text-align: center;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.stats {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.music-card {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
.source-banner {
|
|
background: #2a2a2a;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
margin: 20px 0;
|
|
text-align: center;
|
|
border: 1px solid #333333;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body style="background-color: var(--bg-color); color: var(--text-color); font-family: 'JetBrains Mono', monospace;">
|
|
<div class="container">
|
|
<h1>🎵 Halhadus' Local Wrapped</h1>
|
|
<a href="/index.html" class="play-button">← Main Page</a>
|
|
<div class="source-banner">
|
|
<a href="https://git.halhadus.rocks/Halhadus/my-local-wrapped" style="color: #ffffff; text-decoration: none;">📁 Source Codes</a>
|
|
</div>
|
|
|
|
<div class="year-selector">
|
|
{% for y in available_years %}
|
|
<a href="?year={{ y }}" class="year-button {% if y == selected_year %}active{% endif %}">
|
|
{{ y }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<h2>📊 Statistics for {{ selected_year }}</h2>
|
|
<p>⏳ Total Play Time: {{ total_time }} minutes</p>
|
|
<p>🕒 Last Updated: {{ last_updated }}</p>
|
|
</div>
|
|
|
|
<h2>🎶 Most Played Tracks</h2>
|
|
{% for music in top_tracks %}
|
|
<div class="music-card">
|
|
{% if is_youtube_id(music.video_id) %}
|
|
<img src="https://img.youtube.com/vi/{{ music.video_id }}/hqdefault.jpg"
|
|
class="thumbnail"
|
|
alt="{{ music.name }} cover">
|
|
{% else %}
|
|
<div class="thumbnail" style="background: #333; display: flex; align-items: center; justify-content: center;">
|
|
<span style="color: #666;">No thumbnail available</span>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<h3>{{ music.name }}</h3>
|
|
<p>▶ Played {{ music.count }} times</p>
|
|
<div>
|
|
{% if is_youtube_id(music.video_id) %}
|
|
<a href="https://www.youtube.com/watch?v={{ music.video_id }}"
|
|
class="play-button"
|
|
target="_blank">
|
|
YouTube
|
|
</a>
|
|
<a href="https://music.youtube.com/watch?v={{ music.video_id }}"
|
|
class="play-button"
|
|
target="_blank">
|
|
YT Music
|
|
</a>
|
|
{% else %}
|
|
<a href="{{ music.video_id if music.video_id.startswith('http') else '//' + music.video_id }}"
|
|
class="play-button"
|
|
target="_blank">
|
|
Play Source
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</body>
|
|
</html>'''
|
|
|
|
# Prepare data
|
|
music_data = {row[0]: row for row in read_music_database()}
|
|
count_data = read_count_database(selected_year)
|
|
|
|
top_tracks = []
|
|
for count_entry in count_data:
|
|
if count_entry[0] in music_data:
|
|
video_id = music_data[count_entry[0]][1]
|
|
top_tracks.append({
|
|
'name': count_entry[0],
|
|
'count': count_entry[1],
|
|
'video_id': video_id
|
|
})
|
|
|
|
top_tracks.sort(key=lambda x: x['count'], reverse=True)
|
|
|
|
total_time = int(total_play_time(selected_year) / 60)
|
|
|
|
db_path = f'assets/localwrapped/count-{selected_year}.db' if selected_year != current_year else 'assets/localwrapped/count.db'
|
|
last_updated = datetime.datetime.utcfromtimestamp(os.path.getmtime(db_path)).strftime('%Y-%m-%d %H:%M UTC')
|
|
|
|
return flask.render_template_string(
|
|
html,
|
|
top_tracks=top_tracks,
|
|
total_time=total_time,
|
|
last_updated=last_updated,
|
|
available_years=available_years,
|
|
selected_year=selected_year,
|
|
is_youtube_id=is_youtube_id
|
|
)
|