Guest User

Untitled

a guest
Sep 10th, 2025
38
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.79 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. from pathlib import Path
  3. import xml.etree.ElementTree as ET
  4. from collections import Counter
  5. import html
  6. import sys
  7. import math
  8.  
  9. def extract_playcounts(plist_path: Path):
  10.     parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
  11.     tree = ET.parse(str(plist_path), parser=parser)
  12.     root = tree.getroot()
  13.     top = root.find("dict")
  14.     if top is None:
  15.         raise RuntimeError("Top-level <dict> not found.")
  16.     kids = list(top)
  17.     tracks = None
  18.     for i, e in enumerate(kids):
  19.         if e.tag == "key" and e.text == "Tracks":
  20.             if i+1 < len(kids) and kids[i+1].tag == "dict":
  21.                 tracks = kids[i+1]
  22.                 break
  23.     if tracks is None:
  24.         raise RuntimeError("Tracks <dict> not found.")
  25.  
  26.     artist_counts = Counter()
  27.     album_counts = Counter()
  28.     song_counts = Counter()
  29.  
  30.     tc = list(tracks)
  31.     for i in range(0, len(tc), 2):
  32.         if i+1 >= len(tc): break
  33.         info = tc[i+1]
  34.         if info.tag != "dict": continue
  35.         elems = list(info)
  36.  
  37.         artist = None
  38.         album = None
  39.         name = None
  40.         playcount = 0
  41.  
  42.         for j in range(0, len(elems)-1):
  43.             if elems[j].tag == "key":
  44.                 k = elems[j].text
  45.                 v = elems[j+1]
  46.                 if k == "Artist":
  47.                     artist = (v.text or "").strip()
  48.                 elif k == "Album":
  49.                     album = (v.text or "").strip()
  50.                 elif k == "Name":
  51.                     name = (v.text or "").strip()
  52.                 elif k == "Play Count":
  53.                     try:
  54.                         playcount = int(v.text or "0")
  55.                     except ValueError:
  56.                         playcount = 0
  57.  
  58.         if artist:
  59.             artist_counts[artist] += playcount
  60.         if album:
  61.             album_counts[album] += playcount
  62.         if name:
  63.             song_counts[name] += playcount
  64.  
  65.     return artist_counts, album_counts, song_counts
  66.  
  67.  
  68. def build_css():
  69.     return """
  70. body {
  71.  background: #99ccff;
  72.  font-family: Arial, Helvetica, sans-serif;
  73.  color: #000;
  74.  margin: 0;
  75.  padding: 0;
  76.  text-align: center;
  77. }
  78.  
  79. .wrap {
  80.  width: 800px;
  81.  margin: 0 auto;
  82.  background: #ffffff;
  83.  border: 3px solid #000080;
  84.  box-shadow: 5px 5px 10px #666;
  85.  padding: 20px;
  86. }
  87.  
  88. h1 {
  89.  font-family: Arial, Helvetica, sans-serif;
  90.  font-size: 28px;
  91.  color: #000080;
  92.  text-shadow: 1px 1px #ccc;
  93.  margin-bottom: 20px;
  94. }
  95.  
  96. h2 {
  97.  font-size: 20px;
  98.  color: #333;
  99.  border-bottom: 2px solid #000080;
  100.  padding-bottom: 5px;
  101.  margin-top: 30px;
  102. }
  103.  
  104. a {
  105.  color: #000080;
  106.  text-decoration: underline;
  107. }
  108.  
  109. input {
  110.  width: 90%;
  111.  padding: 5px;
  112.  margin: 10px 0;
  113.  border: 2px inset #ccc;
  114.  font-size: 14px;
  115.  background: #ffffcc;
  116. }
  117.  
  118. table {
  119.  width: 100%;
  120.  border: 2px solid #000080;
  121.  border-collapse: collapse;
  122.  margin-bottom: 20px;
  123. }
  124.  
  125. th {
  126.  background: #3366cc;
  127.  color: #fff;
  128.  font-size: 14px;
  129.  padding: 8px;
  130.  border: 1px solid #000080;
  131. }
  132.  
  133. td {
  134.  background: #e6f0ff;
  135.  border: 1px solid #000080;
  136.  padding: 6px;
  137.  font-size: 13px;
  138. }
  139.  
  140. tbody tr:hover {
  141.  background: #ffff99;
  142. }
  143.  
  144. .footer {
  145.  font-size: 12px;
  146.  color: #666;
  147.  margin-top: 20px;
  148.  border-top: 1px dashed #666;
  149.  padding-top: 10px;
  150. }
  151. """
  152.  
  153.  
  154. def build_table(title, counts, limit=None, table_id=""):
  155.     rows = []
  156.     rank = 1
  157.     items = counts.most_common(limit) if limit else counts.most_common()
  158.     for item, total in items:
  159.         rows.append(f"<tr><td>{rank}</td><td>{html.escape(item)}</td><td>{total}</td></tr>")
  160.         rank += 1
  161.     if not rows:
  162.         rows.append("<tr><td colspan='3'>No data</td></tr>")
  163.     return f"""
  164.    <h2><a href="{title.lower()}_1.html">{title}</a></h2>
  165.    <table id="{table_id}">
  166.      <thead>
  167.        <tr><th style="width:70px">#</th><th>{title[:-1]}</th><th style="width:120px">Plays</th></tr>
  168.      </thead>
  169.      <tbody>
  170.        {''.join(rows)}
  171.      </tbody>
  172.    </table>
  173.    """
  174.  
  175.  
  176. def build_full_page(title, counts, filename, per_page=100):
  177.     css = build_css()
  178.     items = counts.most_common()
  179.     pages = math.ceil(len(items)/per_page)
  180.  
  181.     pages_html = []
  182.     for p in range(pages):
  183.         start = p*per_page
  184.         end = start+per_page
  185.         chunk = items[start:end]
  186.  
  187.         rows = []
  188.         rank = start+1
  189.         for item, total in chunk:
  190.             rows.append(f"<tr><td>{rank}</td><td>{html.escape(item)}</td><td>{total}</td></tr>")
  191.             rank += 1
  192.  
  193.         nav = " ".join(
  194.             f'<a href="{filename.replace(".html","")}_{i+1}.html">[{i+1}]</a>'
  195.             for i in range(pages)
  196.         )
  197.  
  198.         page_html = f"""<!DOCTYPE html>
  199. <html lang="en">
  200. <head>
  201. <meta charset="utf-8">
  202. <title>{title} - Page {p+1}</title>
  203. <style>{css}</style>
  204. </head>
  205. <body>
  206.  <div class="wrap">
  207.    <h1>{title} (Page {p+1} of {pages})</h1>
  208.    <table>
  209.      <thead>
  210.        <tr><th style="width:70px">#</th><th>{title[:-1]}</th><th style="width:120px">Plays</th></tr>
  211.      </thead>
  212.      <tbody>
  213.        {''.join(rows)}
  214.      </tbody>
  215.    </table>
  216.    <div>{nav}</div>
  217.    <div class="footer"><a href="music_stats.html">Back to main page</a></div>
  218.  </div>
  219. </body>
  220. </html>"""
  221.         pages_html.append((f"{filename.replace('.html','')}_{p+1}.html", page_html))
  222.  
  223.     return pages_html
  224.  
  225.  
  226. def build_main_page(artist_counts, album_counts, song_counts):
  227.     css = build_css()
  228.     return f"""<!DOCTYPE html>
  229. <html lang="en">
  230. <head>
  231. <meta charset="utf-8">
  232. <title>Music Stats (2005 Style)</title>
  233. <style>{css}</style>
  234. <script>
  235. function searchAll() {{
  236.  var input = document.getElementById('searchBox');
  237.  var filter = input.value.toLowerCase();
  238.  var tables = ['artistsTable','albumsTable','songsTable'];
  239.  tables.forEach(function(tbl) {{
  240.    var rows = document.querySelectorAll('#' + tbl + ' tbody tr');
  241.    rows.forEach(function(row, index) {{
  242.      var txt = row.cells[1].textContent || row.cells[1].innerText;
  243.      if (filter) {{
  244.        row.style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
  245.      }} else {{
  246.        row.style.display = index < 20 ? '' : 'none';
  247.      }}
  248.    }});
  249.  }});
  250. }}
  251. </script>
  252. </head>
  253. <body>
  254.  <div class="wrap">
  255.    <h1>Top Music Stats</h1>
  256.    <div class="meta">Generated from Winamp/iTunes library</div>
  257.  
  258.    <h2>Search Artists, Albums, or Songs</h2>
  259.    <input type="text" id="searchBox" onkeyup="searchAll()" placeholder="Search for anything...">
  260.  
  261.    {build_table("Artists", artist_counts, limit=20, table_id="artistsTable")}
  262.    {build_table("Albums", album_counts, limit=20, table_id="albumsTable")}
  263.    {build_table("Songs", song_counts, limit=20, table_id="songsTable")}
  264.  
  265.    <div class="footer">Created locally • {len(artist_counts)} artists, {len(album_counts)} albums, {len(song_counts)} songs</div>
  266.  </div>
  267. </body>
  268. </html>"""
  269.  
  270.  
  271. def main():
  272.     in_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("test.txt")
  273.     out_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".")
  274.     out_dir.mkdir(parents=True, exist_ok=True)
  275.  
  276.     artist_counts, album_counts, song_counts = extract_playcounts(in_path)
  277.  
  278.     # Main page
  279.     main_html = build_main_page(artist_counts, album_counts, song_counts)
  280.     (out_dir/"music_stats.html").write_text(main_html, encoding="utf-8")
  281.  
  282.     # Full pages
  283.     for title, counts, filename in [
  284.         ("Artists", artist_counts, "artists.html"),
  285.         ("Albums", album_counts, "albums.html"),
  286.         ("Songs", song_counts, "songs.html"),
  287.     ]:
  288.         pages = build_full_page(title, counts, filename)
  289.         for fname, content in pages:
  290.             (out_dir/fname).write_text(content, encoding="utf-8")
  291.  
  292.     print(f"Wrote output to {out_dir} with {len(artist_counts)} artists, {len(album_counts)} albums, {len(song_counts)} songs.")
  293.  
  294. if __name__ == "__main__":
  295.     main()
  296.  
Advertisement
Add Comment
Please, Sign In to add comment