diff --git a/.env b/.env new file mode 100644 index 0000000..91b2d8d --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +# Discogs API Configuration +# Get your API key from: https://www.discogs.com/settings/developers +# DISCOGS_USER_TOKEN=your_discogs_user_token_here + +# Optional: Discogs User Agent (recommended) +# DISCOGS_USER_AGENT=YourAppName/1.0 +http://yourwebsite.com diff --git a/README.md b/README.md index e69de29..37c48f6 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,141 @@ +# Media Inventory App + +A FastAPI-based web application that allows users to search for different types of media (books, vinyl records, CDs, and cassettes) using external APIs. + +## Features + +- 📚 **Book Search**: Search for books using the OpenLibrary API +- đŸŽĩ **Music Search**: Search for vinyl records, CDs, and cassettes using the Discogs API +- 🎨 **Clean UI**: Bootstrap-based responsive web interface +- 🔍 **Smart Search**: Different search parameters based on media type +- 📱 **Mobile Friendly**: Responsive design that works on all devices + +## APIs Used + +- **OpenLibrary**: For book information and covers +- **Discogs**: For music releases and album art + +## Installation + +1. **Clone the repository**: + ```bash + git clone + cd fastapi-inventory + ``` + +2. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` + +3. **Configure Discogs API (Required for music searches)**: + - Go to [Discogs Developer Settings](https://www.discogs.com/settings/developers) + - Create a new application or use an existing one + - Generate a User Token + - Copy the `.env` file and add your token: + ```bash + # Edit .env file + DISCOGS_USER_TOKEN=your_discogs_user_token_here + DISCOGS_USER_AGENT=YourAppName/1.0 +http://yourwebsite.com + ``` + +4. **Run the application**: + ```bash + python main.py + ``` + + Or using uvicorn directly: + ```bash + uvicorn main:app --reload --host 0.0.0.0 --port 8000 + ``` + +5. **Open your browser** and navigate to: + ``` + http://localhost:8000 + ``` + +## Usage + +1. **Select Media Type**: Choose from Book, Vinyl Record, CD, or Cassette +2. **Enter Search Query**: Type the title, album name, or keywords +3. **Add Artist (Optional)**: For music searches, you can specify an artist to narrow results +4. **Search**: Click the search button to get results +5. **View Details**: Click on the external links to view more information on the source websites + +## Project Structure + +``` +fastapi-inventory/ +├── main.py # FastAPI application +├── requirements.txt # Python dependencies +├── README.md # This file +├── templates/ # Jinja2 HTML templates +│ ├── base.html # Base template +│ ├── index.html # Search form +│ └── results.html # Search results +└── static/ # Static files + └── style.css # Custom CSS styles +``` + +## API Endpoints + +- `GET /`: Main search form +- `POST /search`: Process search requests and return results +- `GET /docs`: FastAPI automatic API documentation +- `GET /redoc`: Alternative API documentation + +## Dependencies + +- **FastAPI**: Modern, fast web framework for building APIs +- **Uvicorn**: ASGI server for running FastAPI +- **httpx**: Async HTTP client for API requests +- **Jinja2**: Template engine for HTML rendering +- **python-multipart**: For handling form data + +## External APIs + +### OpenLibrary API +- **Endpoint**: `https://openlibrary.org/search.json` +- **Usage**: Search for books by title, author, or keywords +- **Rate Limits**: No authentication required, reasonable rate limits + +### Discogs API +- **Endpoint**: `https://api.discogs.com/database/search` +- **Usage**: Search for music releases by title, artist, or format +- **Rate Limits**: No authentication required for basic searches, 60 requests per minute + +## Features in Detail + +### Book Search +- Searches OpenLibrary for book information +- Displays title, author, publication year, and ISBN +- Shows book covers when available +- Links to OpenLibrary pages for more details + +### Music Search +- Searches Discogs for music releases +- Supports vinyl, CD, and cassette formats +- Displays title, artist, year, label, and format information +- Shows album artwork when available +- Links to Discogs pages for more details + +### User Interface +- Clean, modern design using Bootstrap 5 +- Responsive layout that works on desktop and mobile +- Dynamic form that shows/hides artist field based on media type +- Card-based results display with consistent layout +- Error handling and user feedback + +## Development + +To run in development mode with auto-reload: + +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +The application will automatically reload when you make changes to the code. + +## License + +This project is open source and available under the MIT License. diff --git a/main.py b/main.py new file mode 100644 index 0000000..fa9cf75 --- /dev/null +++ b/main.py @@ -0,0 +1,191 @@ +from fastapi import FastAPI, Request, Form +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +import httpx +import asyncio +from typing import Optional +import json +import os +from dotenv import load_dotenv +import discogs_client as discogs + +# Load environment variables +load_dotenv() + +app = FastAPI(title="Media Inventory App", description="Search for books, vinyl, CDs, and cassettes") + +# Mount static files and templates +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +# API endpoints for external services +OPENLIBRARY_SEARCH_URL = "https://openlibrary.org/search.json" +DISCOGS_SEARCH_URL = "https://api.discogs.com/database/search" + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + """Serve the main search form""" + return templates.TemplateResponse("index.html", {"request": request}) + +@app.post("/search") +async def search_media( + request: Request, + media_type: str = Form(...), + query: str = Form(...), + artist: Optional[str] = Form(None) +): + """Search for media based on type and query""" + + if not query.strip(): + return templates.TemplateResponse("index.html", { + "request": request, + "error": "Please enter a search query" + }) + + try: + if media_type == "book": + results = await search_openlibrary(query) + else: # vinyl, cd, cassette + results = await search_discogs(query, media_type, artist) + + return templates.TemplateResponse("results.html", { + "request": request, + "results": results, + "media_type": media_type, + "query": query + }) + + except Exception as e: + return templates.TemplateResponse("index.html", { + "request": request, + "error": f"Search failed: {str(e)}" + }) + +async def search_openlibrary(query: str): + """Search OpenLibrary for books""" + async with httpx.AsyncClient() as client: + params = { + "q": query, + "limit": 10, + "fields": "key,title,author_name,first_publish_year,isbn,cover_i" + } + + response = await client.get(OPENLIBRARY_SEARCH_URL, params=params) + response.raise_for_status() + data = response.json() + + books = [] + for doc in data.get("docs", []): + book = { + "title": doc.get("title", "Unknown Title"), + "author": ", ".join(doc.get("author_name", ["Unknown Author"])), + "year": doc.get("first_publish_year", "Unknown"), + "isbn": doc.get("isbn", [None])[0] if doc.get("isbn") else None, + "cover_url": f"https://covers.openlibrary.org/b/id/{doc.get('cover_i')}-M.jpg" if doc.get('cover_i') else None, + "openlibrary_url": f"https://openlibrary.org{doc.get('key')}" if doc.get('key') else None + } + books.append(book) + + return books + +async def search_discogs(query: str, media_type: str, artist: Optional[str] = None): + """Search Discogs for vinyl, CDs, and cassettes using discogs_client""" + + # Get API credentials from environment + user_token = os.getenv("DISCOGS_USER_TOKEN") + user_agent = os.getenv("DISCOGS_USER_AGENT", "MediaInventoryApp/1.0") + + if not user_token: + # Return a helpful message if no API key is configured + return [{ + "title": "Discogs API Key Required", + "artist": "Configuration Needed", + "year": "N/A", + "label": "Please add your Discogs API key to the .env file", + "format": "See README.md for setup instructions", + "cover_url": None, + "discogs_url": "https://www.discogs.com/settings/developers" + }] + + try: + # Initialize Discogs client + d = discogs.Client(user_agent, user_token=user_token) + + # Map our media types to Discogs format + format_map = { + "vinyl": "Vinyl", + "cd": "CD", + "cassette": "Cassette" + } + + # Build search query + search_query = query + if artist: + search_query = f"{artist} {query}" + + # Search for releases + search_results = d.search( + search_query, + type='release', + format=format_map.get(media_type, "Vinyl") + ) + + releases = [] + # Limit to first 10 results + for i, result in enumerate(search_results): + if i >= 10: + break + + try: + # Extract artist name from title or use separate artist field + title = result.title + artist_name = "Unknown Artist" + + if " - " in title: + parts = title.split(" - ", 1) + artist_name = parts[0] + title = parts[1] if len(parts) > 1 else title + + # Try to get additional details + try: + year = result.year if hasattr(result, 'year') else "Unknown" + labels = [label.name for label in result.labels] if hasattr(result, 'labels') and result.labels else ["Unknown Label"] + formats = [f.get('name', 'Unknown') for f in result.formats] if hasattr(result, 'formats') and result.formats else ["Unknown Format"] + except: + year = "Unknown" + labels = ["Unknown Label"] + formats = ["Unknown Format"] + + release = { + "title": title, + "artist": artist_name, + "year": str(year), + "label": ", ".join(labels), + "format": ", ".join(formats), + "cover_url": result.thumb if hasattr(result, 'thumb') else None, + "discogs_url": result.url if hasattr(result, 'url') else None + } + releases.append(release) + + except Exception as e: + # Skip problematic results but continue processing + continue + + return releases + + except Exception as e: + # Return error information + return [{ + "title": "Search Error", + "artist": "Discogs API", + "year": "N/A", + "label": f"Error: {str(e)}", + "format": "Please check your API configuration", + "cover_url": None, + "discogs_url": "https://www.discogs.com/settings/developers" + }] + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2357369 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +httpx==0.25.2 +jinja2==3.1.2 +python-multipart==0.0.6 +python3-discogs-client==2.8 +python-dotenv==1.1.1 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..1414213 --- /dev/null +++ b/static/style.css @@ -0,0 +1,117 @@ +/* Custom styles for Media Inventory App */ + +body { + background-color: #f8f9fa; +} + +.navbar-brand { + font-weight: bold; + font-size: 1.5rem; +} + +.card { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + border: 1px solid rgba(0, 0, 0, 0.125); + transition: box-shadow 0.15s ease-in-out; +} + +.card:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +.card-img-top { + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.btn { + border-radius: 0.375rem; +} + +.form-control, .form-select { + border-radius: 0.375rem; +} + +.alert { + border-radius: 0.5rem; +} + +/* Loading animation for search button */ +.btn:disabled { + opacity: 0.6; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + padding-left: 15px; + padding-right: 15px; + } + + .card-body { + padding: 1rem; + } +} + +/* Custom media type icons */ +.media-icon { + font-size: 1.2em; + margin-right: 0.5rem; +} + +/* Results grid improvements */ +.card h5 { + font-size: 1.1rem; + line-height: 1.3; + margin-bottom: 0.75rem; +} + +.card-text { + font-size: 0.9rem; + line-height: 1.4; +} + +/* Cover image placeholder styling */ +.card-img-top.bg-light { + background-color: #f8f9fa !important; + border: 2px dashed #dee2e6; +} + +/* Search form enhancements */ +.form-text { + font-size: 0.875rem; + color: #6c757d; +} + +/* Button hover effects */ +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 0.25rem 0.5rem rgba(0, 123, 255, 0.25); +} + +.btn-outline-primary:hover { + transform: translateY(-1px); +} + +/* Card title truncation for long titles */ +.card-title { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + min-height: 2.6rem; +} + +/* Ensure cards have equal height */ +.card.h-100 { + display: flex; + flex-direction: column; +} + +.card-body.d-flex.flex-column { + flex: 1; +} + +.mt-auto { + margin-top: auto !important; +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..cbdc54d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,23 @@ + + + + + + {% block title %}Media Inventory App{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..df4d7ce --- /dev/null +++ b/templates/index.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block title %}Search Media - Media Inventory App{% endblock %} + +{% block content %} +
+
+
+
+

🔍 Search Media

+
+
+ {% if error %} + + {% endif %} + +
+
+ + +
+ +
+ + +
+ For books: Enter book title or author name
+ For music: Enter album title or song name +
+
+ + + + +
+
+
+ +
+
+
+
â„šī¸ How it works
+
+
+
    +
  • 📚 Books: Searches OpenLibrary.org for book information
  • +
  • đŸŽĩ Music (Vinyl/CD/Cassette): Searches Discogs.com for music releases
  • +
  • đŸŽ¯ Tips: Be specific with your search terms for better results
  • +
+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/results.html b/templates/results.html new file mode 100644 index 0000000..3f5ec47 --- /dev/null +++ b/templates/results.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} + +{% block title %}Search Results - Media Inventory App{% endblock %} + +{% block content %} +
+
+
+

Search Results

+ 🔍 New Search +
+ +
+ Query: "{{ query }}" + Media Type: + {% if media_type == 'book' %}📚 Book{% endif %} + {% if media_type == 'vinyl' %}đŸŽĩ Vinyl Record{% endif %} + {% if media_type == 'cd' %}đŸ’ŋ CD{% endif %} + {% if media_type == 'cassette' %}đŸ“ŧ Cassette{% endif %} +
+ + {% if results %} +
+ {% for item in results %} +
+
+ {% if item.cover_url %} + Cover + {% else %} +
+ No Image +
+ {% endif %} + +
+
{{ item.title }}
+ + {% if media_type == 'book' %} +

+ Author: {{ item.author }}
+ Year: {{ item.year }}
+ {% if item.isbn %} + ISBN: {{ item.isbn }}
+ {% endif %} +

+ {% else %} +

+ Artist: {{ item.artist }}
+ Year: {{ item.year }}
+ Label: {{ item.label }}
+ Format: {{ item.format }} +

+ {% endif %} + +
+ {% if media_type == 'book' and item.openlibrary_url %} + + 📚 View on OpenLibrary + + {% elif media_type != 'book' and item.discogs_url %} + + đŸŽĩ View on Discogs + + {% endif %} +
+
+
+
+ {% endfor %} +
+ {% else %} +
+

No results found

+

Try adjusting your search terms or selecting a different media type.

+ 🔍 Try Another Search +
+ {% endif %} +
+
+{% endblock %}