small improvements

This commit is contained in:
2025-07-21 20:44:30 +02:00
parent 5a0727abac
commit b4420e334d
2 changed files with 180 additions and 95 deletions

View File

@ -12,6 +12,11 @@ from PIL import Image
from typing import List, Dict, Any from typing import List, Dict, Any
import pandas as pd import pandas as pd
import re import re
from enum import Enum
class CardStyle(str,Enum):
FULL_ALBUM_COVER = "full album cover"
MINIMAL = "minimal"
class Hipsterfy(object): class Hipsterfy(object):
@ -42,38 +47,25 @@ class HipsterfyPlaylistItem(object):
return img_html return img_html
def generate_hipsterfy_front_card(self, include_preview:bool = False, print_mode:bool = False): def generate_hipsterfy_front_card(self, include_preview:bool = False, card_style:CardStyle = CardStyle.FULL_ALBUM_COVER):
# 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 = """
card_style = """ width: 300px; height: 300px;
width: 300px; height: 300px; border: 1.5px;
border: 1.5px; border-radius: 18px;
border-radius: 18px; display: flex;
display: flex; align-items: center;
align-items: center; justify-content: center;
justify-content: center; background: none;
background: none; margin: 18px;
margin: 18px; box-shadow: none;
box-shadow: none; transition: 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="{card_style}"> <div style="{card_style}">
<div style="text-align: center;"> <div style="text-align: center;">
@ -89,46 +81,108 @@ class HipsterfyPlaylistItem(object):
</div> </div>
""" """
def generate_hipsterfy_back_card(self, primary_property, secondary_property, additional_properties:List[str]=None, album_cover_gb:bool = True, print_mode:bool = False): def generate_hipsterfy_back_card(
# generates the back card with primary and secondary property and additional properties self,
primary_property,
secondary_property,
additional_properties: List[str] = None,
card_style: CardStyle = CardStyle.FULL_ALBUM_COVER
):
additional_html = "" additional_html = ""
if additional_properties: if additional_properties:
for prop in additional_properties: for prop in additional_properties:
value = getattr(self, f"{prop}", "N/A") value = getattr(self, f"{prop}", "N/A")
additional_html += f"<div style='margin: 4px 0; color:#fff;'><strong style='color:#fff;'>{prop.replace('_', ' ').title()}:</strong> <span style='color:#fff;'>{value}</span></div>" additional_html += f"<div style='margin: 4px 0; color:#fff;'><strong style='color:#fff;'>{prop.replace('_', ' ').title()}:</strong> <span style='color:#fff;'>{value}</span></div>"
# Albumcover als Hintergrund # Style für die innere Box (Textfeld)
album_bg = "" inner_box_style_full = """
if album_cover_gb and self.album_images and len(self.album_images) > 0: width: 250px;
album_url = self.album_images[0]['url'] height: 250px;
album_bg = f""" margin: auto;
background: background: rgba(0,0,0,0.35);
linear-gradient(135deg, rgba(30,30,30,0.65) 60%, rgba(0,0,0,0.85) 100%),
url('{album_url}') center center/cover no-repeat;
"""
else:
album_bg = "background: #f9f9f9;"
return f"""
<div style="
width: 300px; height: 300px;
border: 1.5px solid #222;
border-radius: 18px; border-radius: 18px;
padding: 24px 18px 18px 18px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
{album_bg} box-sizing: border-box;
margin: 18px;
box-shadow: 0 6px 24px rgba(0,0,0,0.13), 0 1.5px 4px rgba(0,0,0,0.07);
color: #fff;
overflow: hidden;
position: relative; position: relative;
transition: box-shadow 0.2s; z-index: 1;
"> backdrop-filter: blur(2px);
<div style="text-align: center; padding: 24px 18px 18px 18px; background: rgba(0,0,0,0.35); border-radius: 12px;"> -webkit-backdrop-filter: blur(2px);
<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> inner_box_style_minimal = """
width: 250px;
height: 250px;
margin: auto;
background: #fff;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 24px 18px 18px 18px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
position: relative;
z-index: 1;
"""
# FULL_ALBUM_COVER: Albumcover als Hintergrund, weiße Schrift, abgedunkelte Box
if CardStyle(card_style) == CardStyle.FULL_ALBUM_COVER and self.album_images and len(self.album_images) > 0:
album_url = self.album_images[0]['url']
outer_style = f"""
width: 300px; height: 300px;
border: 1.5px solid #222;
border-radius: 18px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background:
linear-gradient(135deg, rgba(30,30,30,0.65) 60%, rgba(0,0,0,0.85) 100%),
url('{album_url}') center center/cover no-repeat;
margin: 18px;
box-shadow: 0 6px 24px rgba(0,0,0,0.13), 0 1.5px 4px rgba(0,0,0,0.07);
color: #fff;
overflow: hidden;
position: relative;
transition: box-shadow 0.2s;
"""
text_color = "#fff"
additional_html = additional_html.replace("color:#fff", f"color:{text_color}")
primary_color = "#fff"
secondary_color = "#e0e0e0"
inner_box_style = inner_box_style_full
# MINIMAL: Weißer Hintergrund, schwarze Schrift, nur Kontur
else:
outer_style = """
width: 300px; height: 300px;
border: 2px solid #222;
border-radius: 18px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #fff;
margin: 18px;
box-shadow: none;
color: #222;
overflow: hidden;
position: relative;
transition: box-shadow 0.2s;
"""
inner_box_style = inner_box_style_minimal
additional_html = additional_html.replace("color:#fff", "color:#222")
primary_color = "#222"
secondary_color = "#444"
return f"""
<div style="{outer_style}">
<div style="{inner_box_style}">
<p style="margin: 0 0 12px 0; font-size: 1.1em; color: {secondary_color}; text-shadow: none;">{getattr(self, secondary_property)}</p>
<h3 style="margin: 0 0 8px 0; font-size: 1.35em; color: {primary_color}; text-shadow: none;">{getattr(self, primary_property)}</h3>
{additional_html} {additional_html}
</div> </div>
</div> </div>

View File

@ -1,10 +1,13 @@
import panel as pn import panel as pn
import base64 import base64
from io import BytesIO from io import BytesIO
from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem, CardStyle
pn.extension("filedownload", "notifications", "location") 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
@ -12,32 +15,43 @@ def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Templa
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=["artists", "album", "popularity"]) 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) card_style = pn.widgets.Select(name='Card Style', options=[style.value for style in CardStyle], value=CardStyle.FULL_ALBUM_COVER.value)
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=8, value=4, step=1)
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(playlist_uri, {"value": "playlist_uri"})
pn.state.location.sync(primary_item, {"value": "primary_item"}) pn.state.location.sync(primary_item, {"value": "primary_item"})
pn.state.location.sync(secondary_item, {"value": "secondary_item"}) pn.state.location.sync(secondary_item, {"value": "secondary_item"})
pn.state.location.sync(additional_items, {"value": "additional_items"}) pn.state.location.sync(additional_items, {"value": "additional_items"})
pn.state.location.sync(enable_album_art, {"value": "enable_album_art"}) pn.state.location.sync(card_style, {"value": "card_style"})
pn.state.location.sync(cards_per_row_widget, {"value": "cards_per_row"})
create_preview_button = pn.widgets.Button(name='Create Preview', button_type='primary') create_preview_button = pn.widgets.Button(name='Parse Playlist and Generate Cards', button_type='primary', sizing_mode='stretch_width')
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')
# FileDownload-Buttons (werden erst nach Preview-Generierung angezeigt) # FileDownload-Buttons (werden erst nach Preview-Generierung angezeigt)
download_front_html_button = pn.widgets.FileDownload( download_front_html_button = pn.widgets.FileDownload(
label="Download Front Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_front_cards.html" label="Download Front Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_front_cards.html", sizing_mode='stretch_width'
) )
download_back_html_button = pn.widgets.FileDownload( download_back_html_button = pn.widgets.FileDownload(
label="Download Back Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_back_cards.html" label="Download Back Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_back_cards.html", sizing_mode='stretch_width'
)
playlist_instructions = pn.pane.HTML(
"""
<div>
<h3>Playlist Instructions</h3>
<p>Select any Spotify playlist and click the "Parse Playlist and Generate Cards" button to generate the cards.</p>
<p><b>Note:</b> The playlist must be public accessible. Also dynamically individualized playlists (unfortunately a lot of spotify official playlists) Will not work.</p>
</div>
""",
sizing_mode='stretch_width'
) )
print_instructions = pn.pane.HTML( 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);"> <div>
<h3>Print Instructions</h3> <h3>Print Instructions</h3>
<p>To print the cards:</p> <p>To print the cards:</p>
<ol> <ol>
@ -100,48 +114,65 @@ def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Templa
pn.state.notifications.success("Download buttons updated successfully!. Download and print them from the sidebar") 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...") try:
# clear previous cards pn.state.notifications.info("Generating preview for playlist...")
front_cards_column.clear()
back_cards_column.clear()
front_cards_column.append(pn.pane.HTML("<h2>Front Cards</h2>", sizing_mode='stretch_width')) # clear previous cards
back_cards_column.append(pn.pane.HTML("<h2>Back Cards</h2>", sizing_mode='stretch_width')) front_cards_column.clear()
back_cards_column.clear()
# get playlist URI front_cards_column.loading = True
uri = playlist_uri.value.strip() back_cards_column.loading = True
if not uri:
pn.state.notifications.error("Please enter a valid Spotify Playlist URI.")
return
# create HipsterfyPlaylist instance front_cards_column.append(pn.pane.HTML("<h2>Front Cards</h2>", sizing_mode='stretch_width'))
hipsterfy_playlist = HipsterfyPlaylist(uri, hipsterfy) back_cards_column.append(pn.pane.HTML("<h2>Back Cards</h2>", sizing_mode='stretch_width'))
# generate front and back cards for each item in the playlist # get playlist URI
for item in hipsterfy_playlist.get_tracks_data(): uri = playlist_uri.value.strip()
front_card = item.generate_hipsterfy_front_card(include_preview=False, print_mode=print_mode_in_preview.value) if not uri:
back_card = item.generate_hipsterfy_back_card( pn.state.notifications.error("Please enter a valid Spotify Playlist URI.")
primary_property=primary_item.value, return
secondary_property=secondary_item.value,
additional_properties=additional_items.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'))
back_cards_column.append(pn.pane.HTML(back_card, sizing_mode='stretch_width'))
pn.state.notifications.success("Preview generated successfully!") # create HipsterfyPlaylist instance
pn.state.notifications.info("Prepare to download the cards as HTML files...") hipsterfy_playlist = HipsterfyPlaylist(uri, hipsterfy)
update_download_buttons()
new_back_cards = []
new_front_cards = []
# generate front and back cards for each item in the playlist
for item in hipsterfy_playlist.get_tracks_data():
front_card = item.generate_hipsterfy_front_card(include_preview=False, card_style=CardStyle(card_style.value))
back_card = item.generate_hipsterfy_back_card(
primary_property=primary_item.value,
secondary_property=secondary_item.value,
additional_properties=additional_items.value,
card_style=CardStyle(card_style.value)
)
new_front_cards.append(pn.pane.HTML(front_card, sizing_mode='stretch_width'))
new_back_cards.append(pn.pane.HTML(back_card, sizing_mode='stretch_width'))
for card in new_front_cards:
front_cards_column.append(card)
for card in new_back_cards:
back_cards_column.append(card)
pn.state.notifications.success("Preview generated successfully!")
pn.state.notifications.info("Prepare to download the cards as HTML files...")
update_download_buttons()
except Exception as e:
pn.state.notifications.error(f"Error generating preview: {str(e)}")
finally:
front_cards_column.loading = False
back_cards_column.loading = False
create_preview_button.on_click(create_preview) create_preview_button.on_click(create_preview)
template = pn.template.FastListTemplate( template = pn.template.FastListTemplate(
title='Hipsterfy Playlist Manager', title='Hipsterfy',
sidebar=[ sidebar=[
playlist_uri, primary_item, secondary_item, additional_items, enable_album_art, playlist_instructions, playlist_uri, primary_item, secondary_item, additional_items, card_style,
cards_per_row_widget, cards_per_row_widget,
create_preview_button, print_instructions, download_front_html_button, download_back_html_button create_preview_button, download_front_html_button, download_back_html_button, print_instructions
], ],
main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')], main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')],
accent_base_color='indigo', accent_base_color='indigo',