import panel as pn import base64 from io import BytesIO from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem, CardStyle import qrcode import traceback import subprocess pn.extension("filedownload", "notifications", "location") def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None, root_url = None) -> pn.Template: # create widgets 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") 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"]) card_style = pn.widgets.Select(name='Card Style', options=[style.value for style in CardStyle], value=CardStyle.FULL_ALBUM_COVER.value) cards_per_row_widget = pn.widgets.IntSlider(name="Cards per Row in print", start=1, end=8, value=4, step=1) mode_selector = pn.widgets.RadioButtonGroup( name="Card Generation Mode (2D for printing or 3D to produce stl files for 3D printing)", options=["2D", "3D"], value="2D", button_type="primary", button_style="outline", sizing_mode='stretch_width' ) # add a callback that hides the card style widget if 3D mode is selected def toggle_card_style_visibility(event): if mode_selector.value == "3D": card_style.visible = False else: card_style.visible = True mode_selector.param.watch(toggle_card_style_visibility, 'value') 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(card_style, {"value": "card_style"}) pn.state.location.sync(cards_per_row_widget, {"value": "cards_per_row"}) pn.state.location.sync(mode_selector, {"value": "mode_selector"}) 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') back_cards_column = pn.Column(sizing_mode='stretch_width') # FileDownload-Buttons (werden erst nach Preview-Generierung angezeigt) download_front_html_button = pn.widgets.FileDownload( 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( label="Download Back Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_back_cards.html", sizing_mode='stretch_width' ) playlist_instructions = pn.pane.HTML( """

Playlist Instructions

Select any Spotify playlist and click the "Parse Playlist and Generate Cards" button to generate the cards.

Note: The playlist must be public accessible. Also dynamically individualized playlists (unfortunately a lot of spotify official playlists) Will not work.

""", sizing_mode='stretch_width' ) print_instructions = pn.pane.HTML( """

Print Instructions

To print the cards:

  1. Click on the "Download Front Cards" and "Download Back Cards" buttons to download the HTML files.
  2. Open the downloaded files in your web browser.
  3. Use your browser's print function (usually Ctrl+P or Cmd+P) to print the cards.
  4. Make sure to first print all front cards, then all back cards on the back side of the paper.
  5. Adjust the print settings to ensure the cards fit well on the page.
""", sizing_mode='stretch_width' ) print_instructions.visible = False # Initially hidden, will be shown after preview generation def generate_html(cards, row_reverse=False): html = "Print Cards" html += """ """ html += "" 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 += '
' for card in row: html += f'
{card}
' html += '
' 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 download_front_html_button.filename = f"hipsterfy_front_cards.html" download_back_html_button.filename = f"hipsterfy_back_cards.html" 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): try: pn.state.notifications.info("Generating preview for playlist...") # clear previous cards front_cards_column.clear() back_cards_column.clear() front_cards_column.loading = True back_cards_column.loading = True front_cards_column.append(pn.pane.HTML("

Front Cards

", sizing_mode='stretch_width')) back_cards_column.append(pn.pane.HTML("

Back Cards

", sizing_mode='stretch_width')) uri = playlist_uri.value.strip() if not uri: pn.state.notifications.error("Please enter a valid Spotify Playlist URI.") return hipsterfy_playlist = HipsterfyPlaylist(uri, hipsterfy) new_back_cards = [] new_front_cards = [] if mode_selector.value == "2D": # 2D-HTML wie gehabt 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) update_download_buttons() pn.state.notifications.success("2D preview generated successfully!") else: stl_buffers = [] for idx, item in enumerate(hipsterfy_playlist.get_tracks_data()): stl_bytes = item.generate_3d_qr_card( primary_property=primary_item.value, secondary_property=secondary_item.value, additional_properties=additional_items.value, ) stl_filename = f"hipsterfy_card_{idx+1}.stl" stl_buffers.append((stl_filename, stl_bytes)) front_cards_column.append( pn.pane.Markdown(f"**3D Card {idx+1}:** `{stl_filename}` generated.", sizing_mode='stretch_width') ) import zipfile def zip_stls(): try: buffer = BytesIO() with zipfile.ZipFile(buffer, "w") as zf: for fname, stl_bytes in stl_buffers: zf.writestr(fname, stl_bytes) buffer.seek(0) return buffer except Exception as e: print("Error creating ZIP file:", e) traceback.print_exc() pn.state.notifications.error(f"Error creating ZIP file: {str(e)}", duration=10000) return None download_front_html_button.label = "Download 3D Cards (ZIP)" download_front_html_button.callback = zip_stls download_front_html_button.visible = True download_back_html_button.visible = False download_front_html_button.filename = "hipsterfy_3d_cards.zip" pn.state.notifications.success("3D STL files generated! Download the ZIP from the sidebar.") print_instructions.visible = mode_selector.value == "2d" except Exception as e: print("Error:", e) traceback.print_exc() pn.state.notifications.error(f"Error generating preview: {str(e)}", duration=10000) finally: front_cards_column.loading = False back_cards_column.loading = False create_preview_button.on_click(create_preview) # Dynamischer QR-Code für die Player-App def generate_qr_code_base64(url): qr = qrcode.QRCode(box_size=4, border=2) qr.add_data(url) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffered = BytesIO() img.save(buffered, format="PNG") img_str = base64.b64encode(buffered.getvalue()).decode() return f'
Mobile QR-Scanner:
QR Code
{url}
' # Dynamisch: QR-Code aktualisiert sich, wenn die Seite unter einer anderen URL läuft def player_qr_html(): # pn.state.location.href ist erst nach dem ersten Request gesetzt if root_url is None: base_url = pn.state.location.href # Basis-URL ohne evtl. Pfad/Query from urllib.parse import urlparse parsed = urlparse(base_url) base = f"{parsed.scheme}://{parsed.netloc}" else: base = root_url url = base + "/qr/scan.html" return generate_qr_code_base64(url) player_qr_pane = pn.bind(lambda: pn.pane.HTML(player_qr_html(), sizing_mode='stretch_width')) template = pn.template.FastListTemplate( title='Hipsterfy', sidebar=[ player_qr_pane, pn.pane.HTML("

Select card generation mode:

", sizing_mode='stretch_width'), pn.pane.HTML( """

In 2D mode, you can print the cards directly from your browser. In 3D mode, STL files will be generated for 3d printing will be generated.

""", sizing_mode='stretch_width' ), mode_selector, pn.layout.Divider(), playlist_instructions, playlist_uri, primary_item, secondary_item, additional_items, card_style, cards_per_row_widget, 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')], accent_base_color='indigo', header_background='indigo', header_color='white' ) return template