Skip to content

QR Codes

QR Codes¶

🌍 High‑Level Overview¶

The QR blueprint (qr_blueprint.py) provides two public endpoints that generate QR‑code images for a mixtape’s share URL:

Feature Description
Simple QR (/qr/<slug>.png) Returns a plain PNG QR code (optional logo overlay, configurable size).
Enhanced QR (/qr/<slug>/download) Returns a QR code that can embed the mixtape’s cover art and title banner, and is served as a downloadable attachment.
Cache-Control Both endpoints set Cache-Control: public, max-age=3600 so browsers cache the image for one hour.
Graceful fallback If the logo or cover image is missing, the generator falls back to a plain QR code.
Stateless The blueprint only reads data from the injected MixtapeManager and Flask’s static folder; no session data is stored.

🗺️ Flask Blueprint & Routes¶

/qr/<slug>.png¶

Method Description
GET Returns a simple QR code PNG that encodes the public share URL for the mixtape identified by <slug>.

Request flow (simplified)

sequenceDiagram
    participant UI as Browser
    participant Flask as QR Blueprint
    participant MM as MixtapeManager
    participant FS as FileSystem (static folder)

    UI->>Flask: GET /qr/awesome-mixtape.png?size=400&logo=true
    Flask->>MM: mixtape_manager.get('awesome-mixtape')
    alt mixtape exists
        MM-->>Flask: mixtape dict
        Flask->>FS: Resolve logo.svg / logo.png (if requested)
        Flask->>QRGen: generate_mixtape_qr(...)
        QRGen-->>Flask: PNG bytes
        Flask-->>UI: 200 PNG (Cache‑Control: public, max‑age=3600)
    else
        Flask-->>UI: 404 “Mixtape not found”
    end
Hold "Alt" / "Option" to enable pan & zoom

/qr/<slug>/download¶

Method Description
GET Returns an enhanced QR code PNG that can include the mixtape’s cover art and a title banner. The image is served as a downloadable attachment (Content-Disposition: attachment).

Request flow (simplified)

sequenceDiagram
    participant UI as Browser
    participant Flask as QR Blueprint
    participant MM as MixtapeManager
    participant FS as FileSystem (static folder)

    UI->>Flask: GET /qr/awesome-mixtape/download?size=800&include_cover=true&include_title=true
    Flask->>MM: mixtape_manager.get('awesome-mixtape')
    alt mixtape exists
        MM-->>Flask: mixtape dict
        Flask->>FS: Resolve logo.svg / logo.png
        Flask->>FS: Resolve mixtape cover (if requested)
        Flask->>QRGen: generate_mixtape_qr_with_cover(...)
        QRGen-->>Flask: PNG bytes
        Flask-->>UI: 200 PNG (attachment filename="<title>-qr-code.png")
    else
        Flask-->>UI: 404 “Mixtape not found”
    end
Hold "Alt" / "Option" to enable pan & zoom

🕵🏻‍♂️ Query Parameters¶

Parameter Endpoint Type Default Allowed range / values Description
size both integer 400 (simple) / 800 (download) 1–800 (simple), 1–1200 (download) Pixel dimension of the generated QR code (square).
logo simple boolean-like string "true" "true" / "false" (case-insensitive) Whether to overlay the site logo (SVG or PNG) in the centre of the QR.
include_cover download boolean-like string "true" "true" / "false" Include the mixtape’s cover art in the QR (centered).
include_title download boolean-like string "true" "true" / "false" Render the mixtape title as a banner underneath the QR.

Note

All parameters are optional. Missing or malformed values fall back to the defaults.

🗨️ Response Details¶

Status Content-Type Body Headers
200 OK image/png Raw PNG bytes (the QR code). Cache-Control: public, max-age=3600
Content-Disposition:
• Simple QR – inline; filename="<slug>-qr.png"
• Download QR – attachment; filename="<title>-qr-code.png"
404 Not Found application/json { "error": "Mixtape not found" } —
500 Internal Server Error application/json { "error": "Failed to generate QR code" } (or a more specific message if the qrcode library is missing). —

⚠️ Error Handling¶

The blueprint catches two main error families:

  1. Missing third‑party library – If qrcode (or Pillow) is not installed, an ImportError is raised. The handler logs the error and returns 500 with a helpful message:

    QR code generation not available. Install qrcode library.
    
  2. Unexpected runtime errors – Any other exception is logged (logger.exception) and a generic 500 response is sent:

    Failed to generate QR code
    

All 404 cases are handled by checking mixtape_manager.get(slug) and calling abort(404, description="Mixtape not found").

🔀 Internal Workflow¶

sequenceDiagram
    participant UI as Browser
    participant Flask as QR Blueprint
    participant MM as MixtapeManager
    participant QRGen as QR Generator (qr_generator.py)
    participant FS as FileSystem (static folder)

    UI->>Flask: GET /qr/<slug>.png?size=400&logo=true
    Flask->>MM: mixtape_manager.get(slug)
    alt mixtape exists
        MM-->>Flask: mixtape dict
        Flask->>FS: Resolve logo.svg (fallback to logo.png)
        Flask->>QRGen: generate_mixtape_qr(url, title, logo_path, size)
        QRGen-->>Flask: PNG bytes
        Flask-->>UI: 200 PNG (Cache‑Control)
    else
        Flask-->>UI: 404
    end
Hold "Alt" / "Option" to enable pan & zoom

The download endpoint follows the same flow, with two extra steps: resolving the mixtape cover image and calling generate_mixtape_qr_with_cover.

🔧 Configuration & Dependencies¶

Setting Where it lives Default Remarks
qrcode (Python library) project.toml / uv.lock >=7.4 Required for both endpoints.
Pillow (image handling) project.toml >=10.0 Needed for compositing the logo/cover.
Static logo files static/images/logo.svg or static/logo.png — The blueprint prefers SVG; falls back to PNG.
Cover directory app.config["COVER_DIR"] (set in config.py) collection-data/mixtapes/covers Used only by the download endpoint.
Cache-Control Hard-coded in the view (public, max-age=3600). — Adjust in the source if you need a different TTL.

Tip

If you ever need to change the logo location, edit qr_blueprint.py where logo_path is resolved:

logo_path = Path(current_app.static_folder) / "images" / "logo.svg"
if not logo_path.exists():
    logo_path = Path(current_app.static_folder) / "images" / "logo.png"

📌 Example Requests¶

Simple QR (browser)¶

curl -L -o mixtape-qr.png \
  "http://localhost:5000/qr/awesome-mixtape.png?size=400&logo=true"

Result: mixtape-qr.png – a 400 × 400 PNG with the site logo in the centre.

Enhanced QR (download)¶

curl -L -O \
  "http://localhost:5000/qr/awesome-mixtape/download?size=800&include_cover=true&include_title=true"

Result: The server sends a Content‑Disposition: attachment header; the file will be saved as something like Awesome‑Mixtape-qr-code.png.

Using the QR from the Editor UI¶

The editor page (editor.html) contains a Share button (#share-playlist). When the user clicks it, the JavaScript module static/js/editor/qrShare.js:

  1. Retrieves the current mixtape slug (from the hidden #editing-slug input or window.PRELOADED_MIXTAPE).
  2. Opens the QR Share Modal (#qrShareModal).
  3. Sets the <img id="qr-code-img"> source to /qr/<slug>.png?....
  4. Shows a loading spinner until the image loads, then displays the QR.
  5. You can see the full client‑side implementation in editorQrShare.txt.

Using the QR from the Public Player UI¶

The public player page (play_mixtape.html) includes a Share button (#big-share-btn). Its logic lives in static/js/player/qrShare.js:

  • The same modal (#qrShareModal) is reused, but the download endpoint is called (/qr/<slug>/download) when the user clicks the Download button.

Both modules share the same modal markup (see the bottom of editor.html and play_mixtape.html).

🖥️ Front‑End Integration¶

Architecture¶

Component Location Role
QR Modal base.html Global modal (#qrShareModal) available on all pages
Common Module static/js/common/qrShare.js Shared logic for QR display, copying, and downloading
Browser Integration static/js/browser/index.js Initializes QR for multiple mixtapes with autoShow: true
Editor Integration static/js/editor/index.js Initializes QR for single mixtape with autoShow: false (shows after save)
Player Integration static/js/player/index.js Initializes QR for public player with autoShow: true

How It Works¶

  1. Modal defined once - base.html contains the QR modal markup, making it available globally
  2. Common module handles logic - qrShare.js manages modal display, QR loading, copy, and download
  3. Page-specific config - Each page calls initQRShare() with appropriate settings:
// Browser page - multiple share buttons
initQRShare({
    shareButtonSelector: '.qr-share-btn',
    getSlug: (button) => button ? button.dataset.slug : null,
    autoShow: true
});

// Editor page - single share button, hidden until saved
initQRShare({
    shareButtonSelector: '#share-playlist',
    getSlug: () => document.getElementById('editing-slug')?.value,
    autoShow: false  // Shows after save
});

// Player page - single share button
initQRShare({
    shareButtonSelector: '#big-share-btn',
    getSlug: () => extractSlugFromURL(),
    autoShow: true
});

Using the QR from the Editor UI¶

The editor page (editor.html) contains a Share button (#share-playlist). When the user clicks it, the common QR module (static/js/common/qrShare.js):

  1. Retrieves the current mixtape slug (from the hidden #editing-slug input or window.PRELOADED_MIXTAPE)
  2. Opens the QR Share Modal (#qrShareModal) from base.html
  3. Sets the <img id="qr-code-img"> source to /qr/<slug>.png?...
  4. Shows a loading spinner until the image loads, then displays the QR
  5. Provides copy link and download buttons within the modal

Using the QR from the Public Player UI¶

The public player page (play_mixtape.html) includes a Share button (#big-share-btn). The same common module handles the logic:

  • The modal (#qrShareModal) is loaded from base.html
  • The download endpoint (/qr/<slug>/download) is called when the user clicks Download
  • All functionality is identical to the editor page, ensuring consistency

Both pages share the same modal markup (from base.html) and logic (from common/qrShare.js).

🔌 API¶

qr_blueprint ¶

Functions:

Name Description
create_qr_blueprint

Creates Flask blueprint for QR code generation.

create_qr_blueprint(mixtape_manager, logger=None) ¶

Creates Flask blueprint for QR code generation.

Parameters:

Name Type Description Default
mixtape_manager MixtapeManager

MixtapeManager instance for retrieving mixtape data

required
logger Logger | None

Logger instance for error reporting

None

Returns:

Name Type Description
Blueprint Blueprint

Configured Flask blueprint for QR endpoints

Source code in src/routes/qr_blueprint.py
def create_qr_blueprint(
    mixtape_manager: MixtapeManager, logger: Logger | None = None
) -> Blueprint:
    """
    Creates Flask blueprint for QR code generation.

    Args:
        mixtape_manager: MixtapeManager instance for retrieving mixtape data
        logger: Logger instance for error reporting

    Returns:
        Blueprint: Configured Flask blueprint for QR endpoints
    """
    qr = Blueprint("qr", __name__)
    logger: Logger = logger or NullLogger()

    @qr.route("/qr/<slug>.png")
    def generate_qr(slug: str) -> Response:
        """
        Generate a simple QR code PNG for a mixtape share URL.

        Query parameters:
            size: QR code size in pixels (default 400, max 800)
            logo: Include logo in center (default true)
            type: URL type - 'share' for public player or 'gift-playful'/'gift-elegant' for gift experience (default 'share')
            to: Gift recipient name (for gift URLs)
            from: Gift sender name (for gift URLs)
            note: Gift personal message (for gift URLs)

        Args:
            slug: The mixtape slug identifier (URL-encoded)

        Returns:
            Response: PNG image of the QR code
        """
        # URL decode the slug (handles spaces and special characters)
        slug = unquote(slug)

        # Verify mixtape exists
        mixtape = mixtape_manager.get(slug)
        if not mixtape:
            abort(404, description="Mixtape not found")

        # Get parameters
        size = min(int(request.args.get("size", 400)), 800)
        include_logo = request.args.get("logo", "true").lower() != "false"
        url_type = request.args.get("type", "share")

        # Get gift personalization parameters
        gift_to = request.args.get("to", "")
        gift_from = request.args.get("from", "")
        gift_note = request.args.get("note", "")

        # Generate QR code
        try:
            # Get logo path if requested
            logo_path = None
            if include_logo:
                logo_path = Path(current_app.static_folder) / "images" / "logo.svg"
                if not logo_path.exists():
                    logo_path = Path(current_app.static_folder) / "images" / "logo.png"
                    if not logo_path.exists():
                        logo_path = None

            # Build share URL with gift parameters if applicable
            share_url = _get_url(slug=slug, url_type=url_type)

            # Add gift parameters to URL if present
            if url_type in ["gift-playful", "gift-elegant"]:
                params = []
                if gift_to:
                    params.append(f"to={quote(gift_to)}")
                if gift_from:
                    params.append(f"from={quote(gift_from)}")
                if gift_note:
                    params.append(f"note={quote(gift_note)}")

                if params:
                    share_url = f"{share_url}?{'&'.join(params)}"

            qr_bytes = generate_mixtape_qr(
                url=share_url,
                title=mixtape.get("title", "Mixtape"),
                logo_path=logo_path,
                size=size,
            )

            # Create filename based on type
            filename_type = "gift" if url_type.startswith("gift") else "qr"
            filename = f"{slug}-{filename_type}.png"

            response = Response(qr_bytes, mimetype="image/png")
            response.headers["Cache-Control"] = "public, max-age=3600"
            response.headers["Content-Disposition"] = f'inline; filename="{filename}"'

            return response

        except ImportError as e:
            logger.error(f"QR code generation failed - library not installed: {e}")
            abort(
                500,
                description="QR code generation not available. Install qrcode library.",
            )
        except Exception as e:
            logger.exception(f"QR code generation failed: {e}")
            abort(500, description="Failed to generate QR code")

    def _get_url(slug: str, url_type: str=""):
        if url_type not in ["share", "gift-playful", "gift-elegant"]:
            url_type = "share"

        # Build the appropriate URL based on type
        if url_type == "gift-playful":
            share_url = url_for("play.gift_playful", slug=slug, _external=True)
        elif url_type == "gift-elegant":
            share_url = url_for("play.gift_elegant", slug=slug, _external=True)
        else:
            share_url = url_for("play.public_play", slug=slug, _external=True)

        return share_url

    @qr.route("/qr/<slug>/download")
    def download_qr(slug: str) -> Response:
        """
        Download enhanced QR code with cover art and title.

        Query parameters:
            size: QR code size in pixels (default 800, max 1200)
            include_cover: Include mixtape cover art (default true)
            include_title: Include mixtape title banner (default true)
            type: URL type - 'share' for public player or 'gift' for gift experience (default 'share')

        Args:
            slug: The mixtape slug identifier (URL-encoded)

        Returns:
            Response: PNG image as downloadable attachment
        """
        # URL decode the slug (handles spaces and special characters)
        slug = unquote(slug)

        # Verify mixtape exists
        mixtape = mixtape_manager.get(slug)
        if not mixtape:
            abort(404, description="Mixtape not found")

        # Get parameters
        qr_size = min(int(request.args.get("size", 800)), 1200)
        include_cover = request.args.get("include_cover", "true").lower() != "false"
        include_title = request.args.get("include_title", "true").lower() != "false"
        url_type = request.args.get("type", "share")  # 'share' or 'gift'

        try:
            # Get logo path
            logo_path = Path(current_app.static_folder) / "images" / "logo.svg"
            if not logo_path.exists():
                logo_path = Path(current_app.static_folder) / "images" / "logo.png"
                if not logo_path.exists():
                    logo_path = None

            # Get cover path if requested
            cover_path = None
            if include_cover and mixtape.get("cover"):
                cover_filename = mixtape["cover"].split("/")[-1]
                cover_path = Path(current_app.config["COVER_DIR"]) / cover_filename
                if not cover_path.exists():
                    cover_path = None

            # Generate enhanced QR code
            share_url = _get_url(slug=slug, url_type=url_type)
            qr_bytes = generate_mixtape_qr_with_cover(
                url=share_url,
                title=mixtape.get("title", "Mixtape"),
                cover_path=cover_path,
                logo_path=logo_path,
                qr_size=qr_size,
                include_title=include_title,
            )

            # Sanitize title for filename
            title = mixtape.get("title", "mixtape")
            safe_title = "".join(c if c.isalnum() or c in " -_" else "_" for c in title)

            # Create filename based on type
            type_suffix = "gift" if url_type == "gift" else "mixtape"
            filename = f"{safe_title}-{type_suffix}-qr-code.png"

            response = Response(qr_bytes, mimetype="image/png")
            response.headers["Content-Disposition"] = (
                f'attachment; filename="{filename}"'
            )

            return response

        except ImportError as e:
            logger.error(f"QR code generation failed - library not installed: {e}")
            abort(
                500,
                description="QR code generation not available. Install qrcode library.",
            )
        except Exception as e:
            logger.exception(f"QR code download failed: {e}")
            abort(500, description="Failed to generate QR code")

    return qr