initial version

This commit is contained in:
2025-07-20 22:14:06 +02:00
parent 62166624dc
commit 5a0727abac
3 changed files with 145 additions and 35 deletions

View File

@ -31,33 +31,51 @@ class Hipsterfy(object):
class HipsterfyPlaylistItem(object): class HipsterfyPlaylistItem(object):
def _generate_qr_code_base64(self, url): def _generate_qr_code_base64(self, url):
qr: qrcode.QRCode[PilImage | PyPNGImage] = qrcode.QRCode(box_size=2, border=2) qr: qrcode.QRCode[PilImage | PyPNGImage] = qrcode.QRCode(box_size=6, border=6)
qr.add_data(url) qr.add_data(url)
qr.make(fit=True) qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white") img = qr.make_image(fill_color="black", back_color="white")
buffered = BytesIO() buffered = BytesIO()
img.save(buffered, format="PNG") img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode() img_str = base64.b64encode(buffered.getvalue()).decode()
return f'<img src="data:image/png;base64,{img_str}" alt="QR Code" style="margin-top:4px;"/>' img_html = f'<img src="data:image/png;base64,{img_str}" alt="QR Code" style="margin-top: 12px; width: 150px; height: 150px; border-radius: 18px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"/>'
def generate_hipsterfy_front_card(self, include_preview:bool = False): return img_html
def generate_hipsterfy_front_card(self, include_preview:bool = False, print_mode:bool = False):
# generates the square front card only with the centered qr code # generates the square front card only with the centered qr code
preview_html = self.embed_html if include_preview else "" preview_html = self.embed_html if include_preview else ""
if not self._qr_html: if not self._qr_html:
return "" return ""
if print_mode:
# Minimalistisches Layout für Druck: kein Hintergrund, kein Schatten, kein Rahmen
card_style = """
width: 300px; height: 300px;
border: 1.5px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
background: none;
margin: 18px;
box-shadow: none;
transition: none;
"""
else:
card_style = """
width: 300px; height: 300px;
border: 1.5px solid #222;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f9f9f9 60%, #e0e0e0 100%);
margin: 18px;
box-shadow: 0 6px 24px rgba(0,0,0,0.13), 0 1.5px 4px rgba(0,0,0,0.07);
transition: box-shadow 0.2s;
"""
return f""" return f"""
<div style=" <div style="{card_style}">
width: 300px; height: 300px;
border: 1.5px solid #222;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f9f9f9 60%, #e0e0e0 100%);
margin: 18px;
box-shadow: 0 6px 24px rgba(0,0,0,0.13), 0 1.5px 4px rgba(0,0,0,0.07);
transition: box-shadow 0.2s;
">
<div style="text-align: center;"> <div style="text-align: center;">
<div style="margin-top: 8px;"> <div style="margin-top: 8px;">
{self._qr_html} {self._qr_html}
@ -65,13 +83,13 @@ class HipsterfyPlaylistItem(object):
</div> </div>
<div style="margin-top: 18px; color: #888; font-size: 0.95em;"> <div style="margin-top: 18px; color: #888; font-size: 0.95em;">
<span>Scan for Spotify Preview</span> <span>Scan Code to play Sample</span>
</div> </div>
</div> </div>
</div> </div>
""" """
def generate_hipsterfy_back_card(self, primary_property, secondary_property, additional_properties:List[str]=None, album_cover_gb:bool = True): def generate_hipsterfy_back_card(self, primary_property, secondary_property, additional_properties:List[str]=None, album_cover_gb:bool = True, print_mode:bool = False):
# generates the back card with primary and secondary property and additional properties # generates the back card with primary and secondary property and additional properties
additional_html = "" additional_html = ""
if additional_properties: if additional_properties:
@ -109,8 +127,8 @@ class HipsterfyPlaylistItem(object):
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
"> ">
<div style="text-align: center; padding: 24px 18px 18px 18px; background: rgba(0,0,0,0.35); border-radius: 12px;"> <div style="text-align: center; padding: 24px 18px 18px 18px; background: rgba(0,0,0,0.35); border-radius: 12px;">
<h3 style="margin: 0 0 8px 0; font-size: 1.35em; color: #fff; text-shadow: 0 2px 8px #000;">{getattr(self, primary_property)}</h3>
<p style="margin: 0 0 12px 0; font-size: 1.1em; color: #e0e0e0; text-shadow: 0 1px 4px #000;">{getattr(self, secondary_property)}</p> <p style="margin: 0 0 12px 0; font-size: 1.1em; color: #e0e0e0; text-shadow: 0 1px 4px #000;">{getattr(self, secondary_property)}</p>
<h3 style="margin: 0 0 8px 0; font-size: 1.35em; color: #fff; text-shadow: 0 2px 8px #000;">{getattr(self, primary_property)}</h3>
{additional_html} {additional_html}
</div> </div>
</div> </div>

View File

@ -19,8 +19,8 @@ def panel_main():
pn.extension() pn.extension()
# Example usage of Hipsterfy # Example usage of Hipsterfy
playlist_uri = 'https://open.spotify.com/playlist/294v6cT4ZWxtpsKQPZyC5h' # Replace with your playlist URI playlist_uri = 'https://open.spotify.com/playlist/5grJs3PKyLE0cL5NYmkGIF' # Replace with your playlist URI
app = create_panel_page(hipsterfy, playlist_uri) app = lambda: create_panel_page(hipsterfy, playlist_uri)
# Serve the Panel app # Serve the Panel app
pn.serve(app, port=args.port, websocket_origin='*', show=False) pn.serve(app, port=args.port, websocket_origin='*', show=False)

View File

@ -1,36 +1,113 @@
import panel as pn import panel as pn
import base64
from io import BytesIO
from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem
pn.extension("filedownload", "notifications", "location")
def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Template: def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Template:
# create widgets # create widgets
playlist_uri = pn.widgets.TextInput(name='Playlist URI', value=playlist_uri or '', placeholder='Enter Spotify Playlist URI') playlist_uri = pn.widgets.TextInput(name='Playlist URI', value=playlist_uri or '', placeholder='Enter Spotify Playlist URI')
primary_item = pn.widgets.Select(name='Primary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="release_year") primary_item = pn.widgets.Select(name='Primary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="release_year")
secondary_item = pn.widgets.Select(name='Secondary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="title") secondary_item = pn.widgets.Select(name='Secondary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="title")
additional_items = pn.widgets.MultiChoice(name='Additional Items', options=HipsterfyPlaylistItem.get_available_properties(), value=["artist", "album"]) additional_items = pn.widgets.MultiChoice(name='Additional Items', options=HipsterfyPlaylistItem.get_available_properties(), value=["artists", "album", "popularity"])
enable_album_art = pn.widgets.Checkbox(name='Enable Album Art', value=True) enable_album_art = pn.widgets.Checkbox(name='Enable Album Art', value=True)
print_mode_in_preview = pn.widgets.Checkbox(name='Print Mode in preview', value=True)
cards_per_row_widget = pn.widgets.IntSlider(name="Cards per Row in print", start=1, end=6, value=4, step=1)
pn.state.location.sync(playlist_uri, {"value": "playlist_uri"})
pn.state.location.sync(primary_item, {"value": "primary_item"})
pn.state.location.sync(secondary_item, {"value": "secondary_item"})
pn.state.location.sync(additional_items, {"value": "additional_items"})
pn.state.location.sync(enable_album_art, {"value": "enable_album_art"})
create_preview_button = pn.widgets.Button(name='Create Preview', button_type='primary') create_preview_button = pn.widgets.Button(name='Create Preview', button_type='primary')
front_cards_column = pn.Column(sizing_mode='stretch_width') front_cards_column = pn.Column(sizing_mode='stretch_width')
back_cards_column = pn.Column(sizing_mode='stretch_width') back_cards_column = pn.Column(sizing_mode='stretch_width')
# add widgets to the sidebar of the template # FileDownload-Buttons (werden erst nach Preview-Generierung angezeigt)
template = pn.template.FastListTemplate( download_front_html_button = pn.widgets.FileDownload(
title='Hipsterfy Playlist Manager', label="Download Front Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_front_cards.html"
sidebar=[playlist_uri, primary_item, secondary_item, additional_items, enable_album_art, create_preview_button], )
main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')], download_back_html_button = pn.widgets.FileDownload(
accent_base_color='indigo', label="Download Back Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_back_cards.html"
header_background='indigo',
header_color='white'
) )
print_instructions = pn.pane.HTML(
"""
<div style="background-color: #f0f0f0; padding: 12px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h3>Print Instructions</h3>
<p>To print the cards:</p>
<ol>
<li>Click on the "Download Front Cards" and "Download Back Cards" buttons to download the HTML files.</li>
<li>Open the downloaded files in your web browser.</li>
<li>Use your browser's print function (usually Ctrl+P or Cmd+P) to print the cards.</li>
<li>Make sure to first print all front cards, then all back cards on the back side of the paper.</li>
<li>Adjust the print settings to ensure the cards fit well on the page.</li
</ol>
</div>
""",
sizing_mode='stretch_width'
)
print_instructions.visible = False # Initially hidden, will be shown after preview generation
def generate_html(cards, row_reverse=False):
html = "<html><head><title>Print Cards</title>"
html += """
<style>
body { background: white; }
.card-row { display: flex; flex-direction: row; margin-bottom: 24px; }
.card { margin: 12px; }
@media print {
.card { page-break-inside: avoid; }
body, .card, .card-row {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
background: white !important;
}
}
</style>
"""
html += "</head><body>"
cards_per_row = cards_per_row_widget.value # <-- Widget-Wert verwenden
# Karten in Reihen zu je 3 anordnen
for i in range(0, len(cards), cards_per_row):
row = cards[i:i+cards_per_row]
if row_reverse:
row = row[::-1]
html += '<div class="card-row">'
for card in row:
html += f'<div class="card">{card}</div>'
html += '</div>'
html += "</body></html>"
return html.encode() # FileDownload erwartet Bytes
def update_download_buttons():
# Front Cards
cards = [pane.object for pane in front_cards_column[1:]]
download_front_html_button.callback = lambda: BytesIO(generate_html(cards, row_reverse=False))
download_front_html_button.visible = True
# Back Cards
cards_back = [pane.object for pane in back_cards_column[1:]]
download_back_html_button.callback = lambda: BytesIO(generate_html(cards_back, row_reverse=True))
download_back_html_button.visible = True
print_instructions.visible = True # Show print instructions after generating preview
pn.state.notifications.success("Download buttons updated successfully!. Download and print them from the sidebar")
def create_preview(event): def create_preview(event):
pn.state.notifications.info("Generating preview for playlist...")
# clear previous cards # clear previous cards
front_cards_column.clear() front_cards_column.clear()
back_cards_column.clear() back_cards_column.clear()
front_cards_column.append(pn.pane.HTML("<h2>Front Cards</h2>", sizing_mode='stretch_width'))
back_cards_column.append(pn.pane.HTML("<h2>Back Cards</h2>", sizing_mode='stretch_width'))
# get playlist URI # get playlist URI
uri = playlist_uri.value.strip() uri = playlist_uri.value.strip()
if not uri: if not uri:
@ -42,20 +119,35 @@ def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Templa
# generate front and back cards for each item in the playlist # generate front and back cards for each item in the playlist
for item in hipsterfy_playlist.get_tracks_data(): for item in hipsterfy_playlist.get_tracks_data():
front_card = item.generate_hipsterfy_front_card(include_preview=False) front_card = item.generate_hipsterfy_front_card(include_preview=False, print_mode=print_mode_in_preview.value)
back_card = item.generate_hipsterfy_back_card( back_card = item.generate_hipsterfy_back_card(
primary_property=primary_item.value, primary_property=primary_item.value,
secondary_property=secondary_item.value, secondary_property=secondary_item.value,
additional_properties=additional_items.value, additional_properties=additional_items.value,
album_cover_gb=enable_album_art.value album_cover_gb=enable_album_art.value,
print_mode=print_mode_in_preview.value
) )
front_cards_column.append(pn.pane.HTML(front_card, sizing_mode='stretch_width')) front_cards_column.append(pn.pane.HTML(front_card, sizing_mode='stretch_width'))
back_cards_column.append(pn.pane.HTML(back_card, sizing_mode='stretch_width')) back_cards_column.append(pn.pane.HTML(back_card, sizing_mode='stretch_width'))
# notify user of success
# bind the create_preview function to the button click event pn.state.notifications.success("Preview generated successfully!")
create_preview_button.on_click(create_preview) pn.state.notifications.info("Prepare to download the cards as HTML files...")
update_download_buttons()
create_preview_button.on_click(create_preview)
template = pn.template.FastListTemplate(
title='Hipsterfy Playlist Manager',
sidebar=[
playlist_uri, primary_item, secondary_item, additional_items, enable_album_art,
cards_per_row_widget,
create_preview_button, print_instructions, download_front_html_button, download_back_html_button
],
main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')],
accent_base_color='indigo',
header_background='indigo',
header_color='white'
)
# return the template
return template return template