Skip to content

Browse Mixtapes

Browse MixtapesΒΆ

The browse_mixtapes Flask blueprint (routes/browse_mixtapes.py) powers the Mixtapes listing page with advanced search and sorting capabilities, file serving, and redirection to the public player. It explains the routes, authentication flow, interaction with MixtapeManager, and the front-end assets (browse_mixtapes.html, CSS, and JavaScript).

🌐 High-Level Overview¢

Component Responsibility
browse_mixtapes Blueprint (routes/browse_mixtapes.py) Registers all UI-facing routes under the /mixtapes prefix, enforces authentication, handles search/sort parameters, and delegates data access to MixtapeManager.
MixtapeManager (mixtape_manager.py) Reads/writes mixtape JSON files, manages cover images, and provides list_all() for the browse view.
Templates (templates/browse_mixtapes.html) Renders the list of mixtapes with a compact search/sort bar and mixtape cards, each with cover, meta info, and action buttons (edit, play, share, delete).
Static assets (static/css/browse_mixtapes.css, static/js/browser/*.js) Provide responsive styling, hybrid search functionality (instant title + deep track search), sorting logic, and delete-confirmation modal.
Authentication (auth.py) @require_auth decorator and check_auth() helper ensure only logged-in users can reach any route in this blueprint.

πŸ—ΊοΈ Flask Blueprint & RoutesΒΆ

HTTP Method URL Pattern View Function Query Parameters Description
GET /mixtapes/ browse() sort_by, sort_order, search, deep Retrieves all mixtapes (MixtapeManager.list_all()), applies search/sort filters, and renders browse_mixtapes.html.
GET /mixtapes/play/<slug> play(slug) - Redirect to the public player (play.public_play) for the given mixtape slug.
GET /mixtapes/files/<path:filename> files(filename) - Serves static files (JSON, cover images, etc.) from the configured MIXTAPE_DIR.
POST /mixtapes/delete/<slug> delete_mixtape(slug) - Deletes the mixtape JSON and its cover image; returns JSON { success: true } or an error.
before_request β€” blueprint_require_auth() - Runs before every request in this blueprint; redirects unauthenticated users to the landing page (url_for("landing")).

All routes are wrapped with @require_auth (except the before_request hook, which performs the same check).

Query ParametersΒΆ

Sort Parameters:

  • sort_by: Field to sort by (updated_at, created_at, title, track_count)
  • sort_order: Sort direction (asc, desc)
  • Default: sort_by=updated_at&sort_order=desc (most recently modified first)

Search Parameters:

  • search: Search query string
  • deep: Boolean (true/false) indicating deep search mode
  • false or absent: Client-side title filtering only
  • true: Server-side search through tracks, artists, albums, and liner notes

Examples:

/mixtapes/?sort_by=title&sort_order=asc
/mixtapes/?search=rock&deep=true&sort_by=track_count&sort_order=desc

πŸ”’ Authentication & Access ControlΒΆ

  • Decorator β€” @require_auth (imported from auth.py) checks the session for a valid user. If the check fails, the decorator returns a redirect to the login page.
  • Blueprint-wide guard β€” @browser.before_request executes check_auth() for every request hitting this blueprint. This is a defensive second line; even if a route is accidentally left undecorated, the guard will still enforce authentication.

Result: Only logged-in users can view the mixtape list, play a mixtape, download files, or delete a mixtape.

πŸ” Search & Sort FeaturesΒΆ

Hybrid Search SystemΒΆ

The browse page implements a two-tier search system:

  1. Instant Title Search (Client-Side)

    • Filters mixtapes by title as you type
    • No server round-trip required
    • Instant visual feedback
    • Implemented in static/js/browser/search.js
  2. Deep Search (Server-Side)

    • Click "Tracks" button to search within:
    • Track names (song titles)
    • Artist names
    • Album names
    • Mixtape titles
    • Liner notes
    • Implemented in _deep_search_mixtapes() helper function
    • Returns filtered list to template

Sorting SystemΒΆ

Users can sort by 8 different combinations via a single dropdown:

Date Options:

  • πŸ“… Recent First (updated_at desc)
  • πŸ“… Oldest First (updated_at asc)
  • πŸ“… Newest Created (created_at desc)
  • πŸ“… Oldest Created (created_at asc)

Name & Size Options:

  • πŸ”€ A β†’ Z (title asc)
  • πŸ”€ Z β†’ A (title desc)
  • 🎡 Most Tracks (track_count desc)
  • 🎡 Fewest Tracks (track_count asc)

Sorting is applied after search filtering, so search results respect the selected sort order.

πŸ“„ Data Flow & Server-Side LogicΒΆ

Listing Mixtapes (GET /mixtapes/)ΒΆ

  1. Request β†’ Flask routes the request to browse() (protected).
  2. Parse parameters β€” Extract sort_by, sort_order, search, and deep from query string.
  3. Mixtape retrieval β€” mixtape_manager.list_all() reads every *.json file in app.config["MIXTAPE_DIR"] and returns a list of dicts.
  4. Apply search β€” If search and deep=true, filter mixtapes using _deep_search_mixtapes():

    def _deep_search_mixtapes(mixtapes: list[dict], query: str) -> list[dict]:
        """Searches across mixtape metadata and all tracks"""
        query_lower = query.lower()
        results = []
    
        for mixtape in mixtapes:
            # Check title, liner notes
            if query_lower in mixtape.get('title', '').lower():
                results.append(mixtape)
                continue
    
            # Check all tracks (name, artist, album)
            for track in mixtape.get('tracks', []):
                if (query_lower in track.get('track', '').lower() or
                    query_lower in track.get('artist', '').lower() or
                    query_lower in track.get('album', '').lower()):
                    results.append(mixtape)
                    break
    
        return results
    
  5. Apply sorting β€” Sort the filtered list based on sort_by and sort_order:

    if sort_by == 'title':
        mixtapes.sort(key=lambda x: x.get('title', '').lower(),
                     reverse=(sort_order == 'desc'))
    elif sort_by == 'track_count':
        mixtapes.sort(key=lambda x: len(x.get('tracks', [])),
                     reverse=(sort_order == 'desc'))
    # ... etc
    
  6. Template rendering β€” Pass mixtapes, sort_by, sort_order, search_query, and search_deep to template.

  7. HTML output β€” Renders search/sort controls and filtered mixtape cards.

Playing a Mixtape (GET /mixtapes/play/<slug>)ΒΆ

  • The view simply redirects to the public player route defined elsewhere (e.g. play.public_play).

    return redirect(url_for("public_play", slug=slug))
    
  • The client ends up on /play/<slug>#play, where the full mixtape UI is rendered.

Serving Files (GET /mixtapes/files/<filename>)ΒΆ

  • Uses Flask's send_from_directory to serve any file under app.config["MIXTAPE_DIR"].
  • This includes the JSON file (<slug>.json) and cover images (covers/<slug>.jpg).

Deleting a Mixtape (POST /mixtapes/delete/<slug>)ΒΆ

  1. Existence check β€” Verifies that <slug>.json exists; if not, returns 404 with JSON error.
  2. Calls mixtape_manager.delete(slug), which removes the JSON file and any associated cover image (covers/<slug>.jpg).
  3. Returns JSON { "success": true } on success, or { "success": false, "error": "..." } with the appropriate HTTP status on failure.

Error HandlingΒΆ

  • All routes catch generic Exception and log the traceback via the injected logger.
  • Errors are reported to the client as JSON with a descriptive error field and an appropriate HTTP status code (400, 404, 500).

πŸ–₯️ UI Layout (Jinja Template β€” browse_mixtapes.html)ΒΆ

Section Details
Header Compact header with "My Mixtapes" title and a "New" button linking to /editor. Responsive sizing (smaller on mobile).
Search & Sort Bar Compact card containing: β€’ Search input with clear button β€’ "Tracks" button (with tooltip) for deep search β€’ Combined sort dropdown (field + order in one) β€’ Minimal vertical space (~50px desktop, ~70px mobile) β€’ Bootstrap theme-aware styling
Search Result Banner When search is active, shows info banner with query, result count, and "Clear Search" button. Distinguishes between title search and deep search results.
Mixtape Cards Loop over filtered/sorted mixtapes. Each card (.mixtape-item) contains: β€’ Cover (.mixtape-cover) β€’ Title (.mixtape-title) β€’ Meta info (.mixtape-meta) β€’ Action buttons (.action-btn): edit, play, QR share, delete
Empty State Context-aware: β€’ No mixtapes: "No mixtapes yet..." β€’ No search results: "No mixtapes found" with clear search button
Delete Confirmation Modal Modal (#deleteConfirmModal) that asks the user to confirm deletion; populated with the mixtape title via JS.
Delete Success Toast Toast (#deleteSuccessToast) shown after a successful deletion.
JS Entry Point <script type="module" src="{{ url_for('static', filename='js/browser/index.js') }}"></script> β€” wires up search, sort, delete, QR share, and tooltip functionality.

All UI elements use Bootstrap 5 utilities and custom CSS variables (--bs-body-bg, --bs-border-color, --bs-body-color) to stay theme-aware (light/dark modes).

Compact Search/Sort Bar FeaturesΒΆ

Space Efficiency:

  • 60%+ vertical space reduction compared to separate cards
  • Single card with all controls
  • Mobile-optimized with reduced padding

Responsive Layout:

  • Desktop (β‰₯992px): Search input expands to fill space, Tracks + Sort align right
  • Tablet (768-991px): Two-row layout with natural wrapping
  • Mobile (<768px): Stacked controls, full-width inputs
  • Tiny (<576px): Extra compact sizing

Theming:

  • Search icon respects Bootstrap color variables
  • No hardcoded colors (works in light/dark mode)
  • Proper contrast in all themes

Tooltips:

  • Tracks button has tooltip: "Search within song names, artists, and albums"
  • Works on both enabled and disabled states
  • Helps users understand deep search feature

🧱 Static Assets (CSS & JS)¢

browse_mixtapes.cssΒΆ

Search & Sort Styling:

  • Compact card with minimal padding (p-2 on mobile, p-md-3 on desktop)
  • Theme-aware colors using CSS variables
  • Input group styling with seamless borders
  • Combined sort dropdown with emoji icons
  • Responsive adjustments for all screen sizes

Mixtape Cards:

  • Responsive card layout β€” Flexbox with wrapping, subtle shadows, and a hover lift effect
  • Action buttons β€” Circular, color-coded (edit=primary, play=success, share=info, delete=danger). Hover scales the button
  • Mobile adjustments β€” Smaller cover size, reduced button dimensions, and a stacked layout for very narrow viewports (max-width: 480px)

Animations:

  • Slide-down animation for search result banner
  • Smooth hover transitions on cards and buttons
  • Subtle lift effect on card hover

JavaScript ModulesΒΆ

File Exported function(s) Purpose
search.js initSearch() Handles hybrid search: instant title filtering (client-side), deep search button click, clear search, and Enter key support. Disables client-side filtering when viewing deep search results.
sorting.js initSorting() Handles combined sort dropdown changes, updates URL with sort parameters, preserves search state during sort changes.
deleteMixtape.js initDeleteMixtape() Handles the delete workflow: opens the confirmation modal, sends a POST /mixtapes/delete/<slug> request, shows success toast, and reloads the page.
index.js β€” Imports and initializes all modules on DOMContentLoaded: search, sorting, delete, QR share, and Bootstrap tooltips.

Common Module (Shared):

  • ../common/qrShare.js - Provides QR code generation, modal display, copy-to-clipboard, and download functionality. Used across browser, editor, and player pages.

All scripts are ES6 modules (type="module"), ensuring they are loaded after the DOM is ready and that they don't pollute the global namespace.

Search Implementation DetailsΒΆ

Client-Side Title Search:

function filterByTitle(query) {
    // Skip if on deep search results page
    if (isDeepSearchActive) return;

    mixtapeItems.forEach(item => {
        const title = item.querySelector('.mixtape-title').textContent;
        if (title.toLowerCase().includes(query.toLowerCase())) {
            item.style.display = originalDisplays.get(item);
        } else {
            item.style.display = 'none';
        }
    });
}

Deep Search Navigation:

deepSearchBtn.addEventListener('click', () => {
    const url = new URL(window.location.href);
    url.searchParams.set('search', query);
    url.searchParams.set('deep', 'true');
    // Preserve sort parameters
    window.location.href = url.toString();
});

Key Features:

  • Instant filtering respects search state (doesn't interfere with server-side results)
  • Clear button removes both client-side filters and navigates away from deep search
  • Enter key triggers deep search
  • URL parameters maintain complete state (search + sort)

πŸ“Š Class & Sequence DiagramsΒΆ

Class DiagramΒΆ

classDiagram
    class browse_mixtapes_Blueprint {
        +browse()
        +play(slug)
        +files(filename)
        +delete_mixtape(slug)
        +blueprint_require_auth()
        -_deep_search_mixtapes()
    }
    class MixtapeManager {
        +list_all()
        +delete(slug)
    }
    class AuthSystem {
        +require_auth()
        +check_auth()
    }
    class SearchSystem {
        +initSearch()
        +filterByTitle()
        +deepSearch()
    }
    class SortSystem {
        +initSorting()
        +updateURL()
    }

    browse_mixtapes_Blueprint --> MixtapeManager : uses
    browse_mixtapes_Blueprint --> AuthSystem : enforces authentication
    browse_mixtapes_Blueprint --> SearchSystem : client-side filtering
    browse_mixtapes_Blueprint --> SortSystem : URL parameter management
Hold "Alt" / "Option" to enable pan & zoom

Sequence Diagram - Searching and Sorting MixtapesΒΆ

sequenceDiagram
    participant User
    participant BrowserJS
    participant FlaskApp
    participant Blueprint as browse_mixtapes
    participant MixtapeManager

    User->>BrowserJS: Types in search box
    BrowserJS->>BrowserJS: Filter by title (instant)

    User->>BrowserJS: Clicks "Tracks" button
    BrowserJS->>FlaskApp: GET /mixtapes/?search=rock&deep=true&sort_by=title&sort_order=asc
    FlaskApp->>Blueprint: browse(search='rock', deep='true', ...)
    Blueprint->>MixtapeManager: list_all()
    MixtapeManager-->>Blueprint: all mixtapes
    Blueprint->>Blueprint: _deep_search_mixtapes(mixtapes, 'rock')
    Blueprint->>Blueprint: sort by title ascending
    Blueprint-->>FlaskApp: render template with filtered results
    FlaskApp-->>User: HTML page with search results
Hold "Alt" / "Option" to enable pan & zoom

Sequence Diagram - Deleting a MixtapeΒΆ

sequenceDiagram
    participant User
    participant BrowserJS
    participant FlaskApp
    participant browse_mixtapes as Blueprint
    participant MixtapeManager

    User->>BrowserJS: Click Delete β†’ opens modal
    BrowserJS->>FlaskApp: POST /mixtapes/delete/<slug>
    FlaskApp->>Blueprint: delete_mixtape(slug)
    Blueprint->>MixtapeManager: delete(slug)
    MixtapeManager-->>Blueprint: success
    Blueprint-->>FlaskApp: JSON {success:true}
    FlaskApp-->>BrowserJS: JSON response
    BrowserJS->>BrowserJS: Show success toast, reload page
Hold "Alt" / "Option" to enable pan & zoom

🎯 User Experience Features¢

Search FlowΒΆ

  1. Quick Title Search:
  2. User types in search box
  3. Results filter instantly (no page reload)
  4. Clear button appears
  5. Tracks button becomes enabled

  6. Deep Search:

  7. User clicks "Tracks" button (or presses Enter)
  8. Page reloads with ?search=query&deep=true
  9. Server searches all metadata
  10. Results include mixtapes with matching songs/artists/albums
  11. Banner shows "Deep search results for: {query}"

  12. Clear Search:

  13. Click clear button (X)
  14. Returns to full mixtape list
  15. Preserves sort settings

Sort FlowΒΆ

  1. Select Sort Option:
  2. User selects from dropdown (e.g., "🎡 Most Tracks")
  3. Page reloads with new sort parameters
  4. Search state is preserved if active

  5. Combined with Search:

  6. Search results can be re-sorted
  7. Sort order maintained during searches
  8. URL reflects complete state

Mobile OptimizationsΒΆ

  • Compact Controls: 60% less vertical space than desktop
  • Touch-Friendly: All buttons meet WCAG size standards
  • Smart Layout: Inputs stack appropriately
  • Visual Clarity: Emoji icons aid recognition
  • Performance: Client-side filtering remains instant

πŸ“Œ APIΒΆ

browse_mixtapes ΒΆ

Functions:

Name Description
create_browser_blueprint

Creates and configures the Flask blueprint for browsing, playing, and managing mixtapes.

create_browser_blueprint(mixtape_manager, func_processing_status, logger=None) ΒΆ

Creates and configures the Flask blueprint for browsing, playing, and managing mixtapes.

Sets up routes for listing mixtapes, serving cover images and files, deleting mixtapes, and handling authentication for all routes in the blueprint.

Parameters:

Name Type Description Default
mixtape_manager MixtapeManager

The manager instance for retrieving and managing mixtapes.

required
func_processing_status any

A function to check the current indexing or processing status.

required
logger Logger

The logger instance for error reporting.

None

Returns:

Name Type Description
Blueprint Blueprint

The configured Flask blueprint for browsing and managing mixtapes.

Source code in src/routes/browse_mixtapes.py
def create_browser_blueprint(
    mixtape_manager: MixtapeManager,
    func_processing_status: any,
    logger: Logger | None = None,
) -> Blueprint:
    """
    Creates and configures the Flask blueprint for browsing, playing, and managing mixtapes.

    Sets up routes for listing mixtapes, serving cover images and files, deleting mixtapes, and handling authentication for all routes in the blueprint.

    Args:
        mixtape_manager (MixtapeManager): The manager instance for retrieving and managing mixtapes.
        func_processing_status (any): A function to check the current indexing or processing status.
        logger (Logger): The logger instance for error reporting.

    Returns:
        Blueprint: The configured Flask blueprint for browsing and managing mixtapes.
    """
    browser = Blueprint("browse_mixtapes", __name__, template_folder="../templates")

    logger: Logger = logger or NullLogger()

    def _deep_search_mixtapes(mixtapes: list[dict], query: str) -> list[dict]:
        """
        Performs a deep search across mixtape tracks, artists, albums, and liner notes.

        Args:
            mixtapes: List of mixtape dictionaries to search
            query: Search query string (case-insensitive)

        Returns:
            Filtered list of mixtapes matching the search query
        """
        if not query:
            return mixtapes

        query_lower = query.lower()
        results = []

        for mixtape in mixtapes:
            # Search in mixtape title
            if query_lower in mixtape.get('title', '').lower():
                results.append(mixtape)
                continue

            # Search in liner notes
            if query_lower in mixtape.get('liner_notes', '').lower():
                results.append(mixtape)
                continue

            # Search within tracks
            tracks = mixtape.get('tracks', [])
            found = False
            for track in tracks:
                # Search track name
                if query_lower in track.get('track', '').lower():
                    found = True
                    break
                # Search artist
                if query_lower in track.get('artist', '').lower():
                    found = True
                    break
                # Search album
                if query_lower in track.get('album', '').lower():
                    found = True
                    break

            if found:
                results.append(mixtape)

        return results

    @browser.route("/")
    @require_auth
    def browse() -> Response:
        """
        Renders the browse mixtapes page or indexing progress if active.

        Checks for ongoing indexing and shows progress if active. Otherwise, lists all mixtapes.
        Supports sorting by title, date (created/updated), and track count with ascending/descending order.
        Supports search by title (client-side) and deep search by tracks/artists/albums (server-side).

        Returns:
            Response: The rendered template for mixtapes or indexing progress.
        """
        from flask import request

        # Get sorting parameters from query string
        sort_by = request.args.get('sort_by', 'updated_at')  # default: most recent
        sort_order = request.args.get('sort_order', 'desc')  # default: descending
        search_query = request.args.get('search', '').strip()
        search_deep = request.args.get('deep', '').lower() == 'true'

        mixtapes = mixtape_manager.list_all()

        # Apply deep search if requested (searches within tracks, artists, albums)
        if search_query and search_deep:
            mixtapes = _deep_search_mixtapes(mixtapes, search_query)

        # Apply sorting
        if sort_by == 'title':
            mixtapes.sort(key=lambda x: x.get('title', '').lower(), 
                         reverse=(sort_order == 'desc'))
        elif sort_by == 'created_at':
            mixtapes.sort(key=lambda x: x.get('created_at') or '', 
                         reverse=(sort_order == 'desc'))
        elif sort_by == 'updated_at':
            mixtapes.sort(key=lambda x: x.get('updated_at') or x.get('created_at') or '', 
                         reverse=(sort_order == 'desc'))
        elif sort_by == 'track_count':
            mixtapes.sort(key=lambda x: len(x.get('tracks', [])), 
                         reverse=(sort_order == 'desc'))

        return render_template("browse_mixtapes.html", 
                             mixtapes=mixtapes,
                             sort_by=sort_by,
                             sort_order=sort_order,
                             search_query=search_query,
                             search_deep=search_deep)

    @browser.route("/play/<slug>")
    @require_auth
    def play(slug: str) -> Response:
        """
        Redirects to the public play page for a given mixtape slug.

        Takes the mixtape slug and redirects the user to the corresponding public play route.

        Args:
            slug: The unique identifier for the mixtape.

        Returns:
            Response: A redirect response to the public play page for the mixtape.
        """
        return redirect(url_for("public_play", slug=slug))

    @browser.route("/files/<path:filename>")
    @require_auth
    def files(filename: str) -> Response:
        """
        Serves a mixtape file from the mixtape directory.

        Returns the requested file as a Flask response for download or display.

        Args:
            filename: The path to the mixtape file within the mixtape directory.

        Returns:
            Response: A Flask response serving the requested file.
        """
        return send_from_directory(current_app.config["MIXTAPE_DIR"], filename)

    @browser.route("/delete/<slug>", methods=["POST"])
    @require_auth
    def delete_mixtape(slug: str) -> Response:
        """
        Deletes a mixtape and its associated cover image.

        Removes the mixtape JSON file and cover image from disk if they exist. Returns a 200 response on success or 404 if the mixtape is not found.

        Args:
            slug: The unique identifier for the mixtape to delete.

        Returns:
            Response: An empty response with status 200 if successful, or 404 if the mixtape does not exist.
        """
        try:
            # First check if it exists
            json_path = mixtape_manager.path_mixtapes / f"{slug}.json"
            if not json_path.exists():
                return jsonify({"success": False, "error": "Mixtape not found"}), 404

            mixtape_manager.delete(slug)
            return jsonify({"success": True}), 200

        except Exception as e:
            logger.exception("Error deleting mixtape")  # if you have logger
            return jsonify({"success": False, "error": str(e)}), 500

    @browser.before_request
    def blueprint_require_auth() -> Response | None:
        """
        Ensures authentication for all routes in the browse_mixtapes blueprint.

        Checks if the user is authenticated before allowing access to any route in this blueprint. Redirects unauthenticated users to the landing page.

        Returns:
            Response or None: A redirect response to the landing page if not authenticated, otherwise None.
        """
        # Protect everything in this blueprint
        if not check_auth():
            return redirect(url_for("landing"))

    return browser

πŸ”§ ConfigurationΒΆ

The browse functionality relies on the following configuration values:

  • MIXTAPE_DIR: Directory where mixtape JSON files and covers are stored
  • DATA_ROOT: Root directory for application data
  • Session configuration for authentication

🎨 Customization¢

Adding New Sort OptionsΒΆ

  1. Add option to HTML template select dropdown
  2. Update browse() function to handle new sort field
  3. Ensure proper sorting logic (ascending/descending)

Modifying Search BehaviorΒΆ

Client-side (Title Search):

  • Edit static/js/browser/search.js
  • Modify filterByTitle() function

Server-side (Deep Search):

  • Edit _deep_search_mixtapes() in routes/browse_mixtapes.py
  • Add or modify search fields

ThemingΒΆ

All colors use CSS custom properties:

.input-group-text {
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
}

Override these in your theme CSS to customize appearance.

πŸ“± Responsive BreakpointsΒΆ

Breakpoint Width Layout Changes
Desktop β‰₯992px Search expands, controls align right, single row
Tablet 768-991px Two-row layout, natural wrapping
Mobile 576-767px Stacked pairs, larger touch targets
Tiny <576px Full stack, extra compact sizing