Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions freetar/backend.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import waitress
import io
import os
from flask import Flask, render_template, request

from flask import Flask, render_template, request, send_file
from flask_caching import Cache
from flask_minify import Minify

from freetar.ug import Search, ug_tab
from freetar.ug import Search, ug_tab, SongDetail
from freetar.utils import get_version, FreetarError
from freetar.chordpro import song_to_chordpro

cache = Cache(config={'CACHE_TYPE': 'SimpleCache',
"CACHE_DEFAULT_TIMEOUT": 0,
Expand Down Expand Up @@ -64,6 +67,44 @@ def show_tab2(tabid: int):
title=f"{tab.artist_name} - {tab.song_name}")


@app.route("/download/<artist>/<song>")
def download_tab(artist: str, song: str):
tab = ug_tab(f"{artist}/{song}")
format = request.args.get('format')
return tab_to_dl_file(tab, format)


@app.route("/download/<tabid>")
def download_tab2(tabid: int):
tab = ug_tab(tabid)
format = request.args.get('format')
return tab_to_dl_file(tab, format)


@app.route("/favs")
def show_favs():
return render_template("index.html",
title="Freetar - Favorites",
favs=True)


def tab_to_dl_file(tab: SongDetail, format: str):
if format == 'ug_txt':
ext = 'ug.txt'
content = tab.raw_tab
elif format == 'txt':
ext = 'txt'
content = tab.plain_text()
elif format == 'chordpro':
ext = 'cho'
content = song_to_chordpro(tab)
else:
return f'no such format: {format}', 400

filename = f'{tab.artist_name} - {tab.song_name}.{ext}'
data = io.BytesIO(content.encode('utf-8'))
return send_file(data, as_attachment=True, download_name=filename)

@app.route("/about")
def show_about():
return render_template('about.html')
Expand Down
170 changes: 170 additions & 0 deletions freetar/chordpro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import json
import re
from dataclasses import dataclass, field

from freetar.ug import SongDetail

def song_to_chordpro(song: SongDetail):
tab_lines = untokenise_tab(intersperse_chords(tokenise_tab(song.raw_tab)))
header_lines = [
chordpro_directive('title', song.song_name),
chordpro_directive('artist', song.artist_name),
chordpro_meta('capo', song.capo),
chordpro_meta('key', song.key),
chordpro_meta('tuning', song.tuning),
chordpro_meta('version', song.version),
chordpro_meta('difficulty', song.difficulty),
]
return ''.join((line + '\n' for line in (header_lines + tab_lines + ['']) if line is not None))

def chordpro_meta(key: str, value: str):
if not value:
return None
if type(value) is not str:
value = str(value)
return chordpro_directive('meta', key + ' ' + value)

def chordpro_directive(name: str, argstr: str = None):
if argstr:
return '{' + name + ': ' + argstr + '}'
else:
return '{' + name + '}'

@dataclass
class Chord():
text: str
pos: int

def __str__(self):
return '[' + self.text + ']'

@dataclass
class Section():
text: str

def id(self):
text = re.sub(r'\s+$', '_', self.text.lower())
text = re.sub(r'[^a-z_]*', '', text)
text = re.sub(r'^verse_[0-9]*$', 'verse', text)
text = re.sub(r'_*$', '', text)
return text

def label(self):
return self.text

@dataclass
class SectionStart():
sec: Section

def __str__(self):
return chordpro_directive('start_of_' + self.sec.id(), self.sec.label())

@dataclass
class SectionEnd():
sec: Section

def __str__(self):
return chordpro_directive('end_of_' + self.sec.id())

@dataclass
class Instrumental():
line: list

def __str__(self):
return chordpro_directive('c', untokenise_line(self.line))

def tokenise_line(line: str):
section_match = re.match(r'^\s*\[([^\[\]]*)\]\s*$', line)
if section_match:
return SectionStart(Section(section_match.group(1)))
return list(tokenise_symbols(line))

def tokenise_symbols(line: str):
pos = 0
while len(line) > 0:
chord_match = re.match(r'^\[ch\]([^[]*)\[\/ch\](.*)$', line)
if chord_match:
line = chord_match.group(2)
yield Chord(text=chord_match.group(1), pos=pos)
pos += len(chord_match.group(1))
else:
c, line = line[0], line[1:]
pos += 1
yield c.replace('[', '(').replace(']', ')')


def insert_chords_between_tokens(chords: list, line: list):
for i, x in enumerate(line):
while chords and chords[0].pos <= i:
yield chords[0]
chords = chords[1:]
yield x

yield from chords

def only_whitespace(line):
return type(line) is list and all((type(x) is str and x.isspace() for x in line))

def only_chords(line):
return type(line) is list and all((type(x) is Chord or (type(x) is str and x.isspace()) for x in line))

def has_chords(line):
return type(line) is list and any((type(x) is Chord for x in line))

def has_lyrics_and_nothing_else(line):
return type(line) is list and (not has_chords(line)) and (not only_whitespace(line))

def intersperse_chords(tlines):
skip = True
for this, next in zip([None] + tlines, tlines + [None]):
if skip:
skip = False
continue
elif has_chords(this) and only_chords(this) and (has_lyrics_and_nothing_else(next)):
yield list(insert_chords_between_tokens([x for x in this if type(x) is Chord], next))
skip = True
elif has_chords(this):
yield Instrumental(this)
else:
yield this

def untokenise_line(line):
if type(line) is list:
return ''.join((str(x) for x in line))
return str(line)

def insert_section_ends(tlines):
cur_sec = None
for line in tlines:
if type(line) is SectionStart:
if cur_sec:
yield SectionEnd(cur_sec)
cur_sec = line.sec
yield line

if cur_sec:
yield SectionEnd(cur_sec)

def move_section_borders(tlines):
i = 0
while i < len(tlines) - 1:
i = 0
while i < len(tlines) - 1:
if only_whitespace(tlines[i]) and type(tlines[i + 1]) is SectionEnd:
tlines[i], tlines[i + 1] = tlines[i + 1], tlines[i]
break
if only_whitespace(tlines[i + 1]) and type(tlines[i]) is SectionStart:
tlines[i], tlines[i + 1] = tlines[i + 1], tlines[i]
break
i += 1
return tlines


def untokenise_tab(tlines):
tlines = move_section_borders(list(insert_section_ends(tlines)))
return [untokenise_line(line) for line in tlines]

def tokenise_tab(tab):
tab = tab.replace("[tab]", "")
tab = tab.replace("[/tab]", "")
return [tokenise_line(line) for line in tab.split('\n')]
8 changes: 8 additions & 0 deletions freetar/static/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ document.getElementById("checkbox_view_chords")?.addEventListener("click", funct
}
});

document.getElementById("download")?.addEventListener("click", function () {
document.getElementById("download-options").style.display = "block";
});

document.getElementById("download-options")?.addEventListener("click", function () {
document.getElementById("download-options").style.display = "none";
});

document.getElementById("dark_mode")?.addEventListener("click", function () {
if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
document.documentElement.setAttribute('data-bs-theme', 'light');
Expand Down
6 changes: 6 additions & 0 deletions freetar/templates/tab.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
<h5>
<a href="/search?search_term={{ tab.artist_name }}">{{ tab.artist_name }}</a> - {{ tab.song_name }} (ver {{tab.version }})
<span title="add/remove song to/from favs" class="favorite m-2 d-print-none" data-artist="{{tab.artist_name}}" data-song="{{tab.song_name}}" data-type="{{tab._type}}" data-rating="{{tab.rating}}" data-url="{{ request.path }}">★</span>
<span title="download" role="button" id="download" class="m-2 d-print-none">📥</span><br/>
<div id="download-options" style="display: none;">
<a class="d-print-none d-none d-md-inline" href="{{ tab.download_url() }}?format=txt">Download (Plain text)</a><br>
<a class="d-print-none d-none d-md-inline" href="{{ tab.download_url() }}?format=ug_txt">Download (UG format)</a><br>
<a class="d-print-none d-none d-md-inline" href="{{ tab.download_url() }}?format=chordpro">Download (ChordPro)</a>
</div>
</h5>
</div>

Expand Down
22 changes: 19 additions & 3 deletions freetar/ug.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ class SongDetail:
version: int
difficulty: str
capo: str
key: str
tuning: str
tab_url: str
tab_url_path: str
alternatives: list[SearchResult] = field(default_factory=list)

def __init__(self, data: dict):
self.tab = data["store"]["page"]["data"]["tab_view"]["wiki_tab"]["content"]
self.raw_tab = data["store"]["page"]["data"]["tab_view"]["wiki_tab"]["content"].replace('\r\n', '\n')
self.artist_name = data["store"]["page"]["data"]["tab"]['artist_name']
self.key = data["store"]["page"]["data"]["tab"].get('tonality_name')
self.song_name = data["store"]["page"]["data"]["tab"]["song_name"]
self.version = int(data["store"]["page"]["data"]["tab"]["version"])
self._type = data["store"]["page"]["data"]["tab"]["type"]
Expand All @@ -60,7 +63,11 @@ def __init__(self, data: dict):
self.capo = data["store"]["page"]["data"]["tab_view"]["meta"].get("capo")
_tuning = data["store"]["page"]["data"]["tab_view"]["meta"].get("tuning")
self.tuning = f"{_tuning['value']} ({_tuning['name']})" if _tuning else None
else:
self.capo = None
self.tuning = None
self.tab_url = data["store"]["page"]["data"]["tab"]["tab_url"]
self.tab_url_path = urlparse(self.tab_url).path
self.alternatives = []
for alternative in data["store"]["page"]["data"]["tab_view"]["versions"]:
if alternative.get("type", "") != "Official":
Expand All @@ -71,8 +78,7 @@ def __repr__(self):
return f"{self.artist_name} - {self.song_name}"

def fix_tab(self):
tab = self.tab
tab = tab.replace("\r\n", "<br/>")
tab = self.raw_tab
tab = tab.replace("\n", "<br/>")
tab = tab.replace(" ", "&nbsp;")
tab = tab.replace("[tab]", "")
Expand All @@ -85,6 +91,13 @@ def fix_tab(self):
tab = re.sub(r'\[ch\](?P<root>[A-Ha-h](#|b)?)(?P<quality>[^[/]+)?(?P<bass>/[A-Ha-h](#|b)?)?\[\/ch\]', self.parse_chord, tab)
self.tab = tab

def plain_text(self):
tab = self.raw_tab
tab = tab.replace("[tab]", "")
tab = tab.replace("[/tab]", "")
tab = re.sub(r'\[ch\]([^\[]*)\[\/ch\]', lambda match: match.group(1), tab)
return tab

def parse_chord(self, chord):
root = '<span class="chord-root">%s</span>' % chord.group('root')
quality = ''
Expand All @@ -95,6 +108,9 @@ def parse_chord(self, chord):
bass = '/<span class="chord-bass">%s</span>' % chord.group('bass')[1:]
return '<span class="chord fw-bold">%s</span>' % (root + quality + bass)

def download_url(self):
return '/download/' + self.tab_url_path.split('/', 2)[2]


@dataclass
class Search:
Expand Down