diff --git a/docs/INDEX.md b/docs/INDEX.md index 7692746..1d9ae06 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -22,6 +22,8 @@ Goondex is a fast, local-first media indexer for adult content. It ingests metad ### Integration - [TPDB Integration](TPDB_INTEGRATION.md) - ThePornDB API integration guide +- [Adult Empire Scraper](ADULT_EMPIRE_SCRAPER.md) - Adult Empire scraper implementation +- [JAV Studios Reference](JAV_STUDIOS_REFERENCE.md) - Japanese Adult Video studios quick reference - [Scraper System](SCRAPER_SYSTEM.md) - How scrapers work - [Adding New Sources](ADDING_SOURCES.md) - Implementing new data sources diff --git a/docs/JAV_STUDIOS_REFERENCE.md b/docs/JAV_STUDIOS_REFERENCE.md new file mode 100644 index 0000000..d110f28 --- /dev/null +++ b/docs/JAV_STUDIOS_REFERENCE.md @@ -0,0 +1,74 @@ +# JAV Studios Quick Reference + +**Last Updated:** December 28, 2025 +**Status:** Planning phase - for future JAV scraper implementation + +This document provides a quick reference for Japanese Adult Video (JAV) studios, their code patterns, specialties, and websites. This information will be used when implementing JAV scrapers for Goondex. + +--- + +## Uncensored Studios (No Mosaic) + +| Studio | Code/Abbrev | Specialties | Website | +|-----------------|-----------------|--------------------------------------|----------------------------------| +| FC2 PPV | FC2PPV | Amateur, creampie, gyaru | adult.fc2.com | +| 1pondo | 1Pondo | High-prod, GF roleplay, creampie | 1pondo.tv | +| Caribbeancom | Caribbeancom | MILF, amateur, big tits, anal | caribbeancom.com | +| HEYZO | HEYZO | Mature, taboo, creampie | en.heyzo.com | +| Pacopacomama | Pacopacomama | Mature housewife, sensual | pacopacomama.com | +| Tokyo Hot | Tokyo Hot | Hardcore, gangbang, extreme | tokyo-hot.com | +| 10musume | 10musume | Real amateurs, pickup | 10musume.com | + +--- + +## Censored Studios (Mosaic Required) + +| Studio | Code/Abbrev | Specialties | Website | +|-----------------|-----------------|--------------------------------------|----------------------------------| +| Moodyz | MIAA, MIDE | Variety, drama, idol, creampie | moodyz.com | +| S1 No.1 Style | SONE, SSIS | Luxury idols, high production | s1s1s1.com | +| Prestige | ABP, ABW | Amateur-style, POV, beautiful girls | prestige-av.com | +| Idea Pocket | IPZZ, IPX | Beautiful idols, aesthetics | ideapocket.com | +| SOD Create | SDDE, SDMU | Variety, gimmick, experimental | sod.co.jp | +| Madonna | JUQ, JUX | Mature housewife, drama | madonna-av.com | +| Attackers | RBD, SHKD | Hardcore, intense, dark drama | attackers.net | +| Fitch | JUFD | Mature, big tits | fitch-av.com | + +--- + +## Notes for Scraper Implementation + +### Code Pattern Recognition + +JAV studios use consistent code patterns for their releases: +- **Uncensored:** Often use studio-specific codes (e.g., FC2PPV-XXXXXX, 1Pondo-XXXXXX) +- **Censored:** Typically use letter codes followed by numbers (e.g., SSIS-XXX, MIAA-XXX) + +### Important Considerations + +1. **Censorship Status:** Track whether content is censored or uncensored in the database +2. **Multiple Codes:** Some studios use multiple code prefixes (e.g., Moodyz uses MIAA, MIDE, etc.) +3. **Code Evolution:** Studio codes may change over time as branding evolves +4. **Website Access:** Some sites may require region-specific access or age verification + +### Future Scraper Architecture + +When implementing JAV scrapers: +- Create separate scraper modules for each major studio +- Implement code pattern matching for automatic studio detection +- Handle both censored and uncensored content appropriately +- Consider rate limiting for scraper requests to avoid blocking +- Implement metadata standardization across different studios + +--- + +## Related Documentation + +- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall system architecture +- [DATABASE_SCHEMA.md](DATABASE_SCHEMA.md) - Database schema including scene metadata +- [ADULT_EMPIRE_SCRAPER.md](ADULT_EMPIRE_SCRAPER.md) - Example scraper implementation +- [TPDB_INTEGRATION.md](TPDB_INTEGRATION.md) - TPDB integration patterns + +--- + +**Note:** For full details and current information, always refer to official studio websites. This is a quick reference guide only. diff --git a/docs/TODO.md b/docs/TODO.md index 6ea11d8..5d98c34 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,13 +1,14 @@ # Goondex TODO / DONE -## TODO -- [ ] Implement bulk studio import (`./goondex import all-studios`) with the same pagination/resume flow as the performer importer. -- [ ] Implement bulk scene import (`./goondex import all-scenes`) and wire the CLI/UI to the new data set. +## TODO (v0.1.0-dev4+) +- [ ] Add image ingestion pipeline (WebP downscale, cached thumbs) for performers (multi-image support) and scenes; make it non-blocking with concurrency caps. +- [ ] Add image backfill/enrichment command for performers/scenes (fetch missing thumbs, skip existing). - [ ] Build a movie ingest path (TPDB and/or Adult Empire) that feeds the `movies` tables and populates the movies pages. - [ ] Align the web stack on a single CSS pipeline (deprecate legacy `style.css`, keep goondex + scoped component files). - [ ] Add lightweight UI validation (lint/smoke tests) for navigation, modals, and search to catch regressions early. ## DONE +- [x] Bulk performer/studio/scene imports paginate until empty (ignore TPDB 10k cap) to maximize coverage. - [x] Split card styling into per-context files (base, performers, studios, scenes) and updated listing templates to use them. - [x] Created shared task lists (`docs/TODO.md`, `docs/WEB_TODO.md`) to keep engineering and web work in sync. - [x] Adult Empire scraper + TPDB merge support for performers (see `SESSION_SUMMARY_v0.1.0-dev4.md`). diff --git a/internal/import/service.go b/internal/import/service.go index f5dabaa..5905343 100644 --- a/internal/import/service.go +++ b/internal/import/service.go @@ -74,6 +74,15 @@ func (s *Service) BulkImportAllPerformersWithProgress(ctx context.Context, progr // Update total on first page if meta != nil && page == 1 { result.Total = meta.Total + if meta.Total >= 10000 { + log.Printf("TPDB performers total reports %d (cap?). Continuing to paginate until empty.", meta.Total) + } + } + + // Stop when no data is returned + if len(performers) == 0 { + log.Printf("No performers returned at page %d; stopping import.", page) + break } // Import each performer @@ -102,11 +111,6 @@ func (s *Service) BulkImportAllPerformersWithProgress(ctx context.Context, progr log.Printf("Imported page %d/%d of performers (%d/%d total)", page, meta.LastPage, result.Imported, result.Total) - // Check if we've reached the last page - if meta == nil || page >= meta.LastPage { - break - } - page++ } @@ -136,6 +140,14 @@ func (s *Service) BulkImportAllStudiosWithProgress(ctx context.Context, progress // Update total on first page if meta != nil && page == 1 { result.Total = meta.Total + if meta.Total >= 10000 { + log.Printf("TPDB studios total reports %d (cap?). Continuing to paginate until empty.", meta.Total) + } + } + + if len(studios) == 0 { + log.Printf("No studios returned at page %d; stopping import.", page) + break } // Import each studio @@ -161,11 +173,6 @@ func (s *Service) BulkImportAllStudiosWithProgress(ctx context.Context, progress log.Printf("Imported page %d/%d of studios (%d/%d total)", page, meta.LastPage, result.Imported, result.Total) - // Check if we've reached the last page - if meta == nil || page >= meta.LastPage { - break - } - page++ } @@ -198,6 +205,14 @@ func (s *Service) BulkImportAllScenesWithProgress(ctx context.Context, progress // Update total on first page if meta != nil && page == 1 { result.Total = meta.Total + if meta.Total >= 10000 { + log.Printf("TPDB scenes total reports %d (cap?). Continuing to paginate until empty.", meta.Total) + } + } + + if len(scenes) == 0 { + log.Printf("No scenes returned at page %d; stopping import.", page) + break } // Import each scene with its performers and tags @@ -279,11 +294,6 @@ func (s *Service) BulkImportAllScenesWithProgress(ctx context.Context, progress log.Printf("Imported page %d/%d of scenes (%d/%d total)", page, meta.LastPage, result.Imported, result.Total) - // Check if we've reached the last page - if meta == nil || page >= meta.LastPage { - break - } - page++ } diff --git a/internal/web/static/css/buttons.css b/internal/web/static/css/buttons.css index d4a164c..6de4982 100644 --- a/internal/web/static/css/buttons.css +++ b/internal/web/static/css/buttons.css @@ -19,23 +19,20 @@ font-weight: 600; cursor: pointer; - color: var(--color-text-primary); - background: var(--color-bg-elevated); + color: #fff; + background: var(--color-brand); - border: 1px solid var(--color-border-soft); + border: 1px solid var(--color-brand); transition: background var(--transition), border-color var(--transition), - box-shadow var(--transition), transform var(--transition-fast); } -/* Hover glow (SUBTLE, medium intensity) */ .btn:hover { - background: var(--color-bg-card); - border-color: var(--color-brand); - box-shadow: var(--shadow-glow-pink-soft); - transform: translateY(-2px); + background: var(--color-brand-hover); + border-color: var(--color-brand-hover); + transform: none; } /* Active press */ @@ -58,21 +55,16 @@ .btn-primary, .btn.brand, .btn.pink { - background: linear-gradient( - 135deg, - var(--color-brand) 0%, - var(--color-brand-hover) 90% - ); + background: linear-gradient(135deg, var(--color-brand), var(--color-brand-hover)); border: none; color: #fff; - text-shadow: 0 0 8px rgba(255, 255, 255, 0.25); + text-shadow: none; } .btn-primary:hover, .btn.brand:hover, .btn.pink:hover { - box-shadow: var(--shadow-glow-pink); - transform: translateY(-2px); + transform: none; } @@ -80,15 +72,30 @@ * SECONDARY BUTTON * ================================ */ .btn-secondary { - background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - color: var(--color-text-primary); + background: transparent; + border: 2px solid var(--color-brand); + color: var(--color-brand); } .btn-secondary:hover { - border-color: var(--color-brand); + border-color: var(--color-brand-hover); + color: var(--color-brand-hover); +} + +/* ================================ + * LIGHT PRIMARY (white bg, pink text) + * ================================ */ +.btn-light-primary { + background: #ffffff; color: var(--color-brand); - box-shadow: var(--shadow-glow-pink-soft); + border: none; +} + +.btn-light-primary:hover { + background: #ffffff; + color: var(--color-brand-hover); + border: none; + transform: none; } @@ -102,7 +109,7 @@ } .btn-small:hover { - transform: translateY(-1px); + transform: none; } diff --git a/internal/web/static/css/cards/card-base.css b/internal/web/static/css/cards/card-base.css index f0a0bad..12eb0d4 100644 --- a/internal/web/static/css/cards/card-base.css +++ b/internal/web/static/css/cards/card-base.css @@ -17,39 +17,24 @@ .gx-card { background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-soft); + border: 1px solid var(--color-border); + border-radius: 20px; overflow: hidden; - box-shadow: var(--shadow-elevated); - transition: - transform var(--transition), - box-shadow var(--transition), - border-color var(--transition); + box-shadow: none; + transition: none; cursor: pointer; position: relative; } -.gx-card:hover { - transform: translateY(-4px); - border-color: var(--color-brand); - box-shadow: - 0 0 18px rgba(255, 79, 163, 0.28), - 0 6px 24px rgba(0, 0, 0, 0.55); -} - .gx-card-thumb { width: 100%; aspect-ratio: var(--gx-card-thumb-ratio); background-size: cover; background-position: center; - filter: brightness(0.92); - transition: filter var(--transition-fast); -} - -.gx-card:hover .gx-card-thumb { - filter: brightness(1); + filter: none; + transition: none; } .gx-card-body { @@ -62,10 +47,7 @@ .gx-card-title { font-size: 1.1rem; font-weight: 600; - - background: linear-gradient(135deg, var(--color-text-primary), var(--color-header)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + color: var(--color-text-primary); } .gx-card-meta { @@ -84,10 +66,10 @@ .gx-card-tag { padding: 0.2rem 0.55rem; font-size: 0.75rem; - border-radius: var(--radius); - background: rgba(255, 79, 163, 0.08); + border-radius: 12px; + background: rgba(255, 79, 163, 0.15); color: var(--color-brand); - border: 1px solid rgba(255, 79, 163, 0.25); + border: 1px solid rgba(255, 79, 163, 0.3); text-transform: uppercase; letter-spacing: 0.03em; } diff --git a/internal/web/static/css/cards/cards-performer.css b/internal/web/static/css/cards/cards-performer.css index b5e5fe9..d6121ac 100644 --- a/internal/web/static/css/cards/cards-performer.css +++ b/internal/web/static/css/cards/cards-performer.css @@ -14,3 +14,21 @@ .performer-card .gx-card-tags { margin-top: 0.6rem; } + +/* Harsh pink style reserved for performer cards */ +.performer-card .gx-card { + background: var(--color-brand); + color: #ffffff; + border: 5px solid #ffffff; +} + +.performer-card .gx-card-title, +.performer-card .gx-card-meta, +.performer-card .gx-card-tag { + color: #ffffff; +} + +.performer-card .gx-card-tag { + background: rgba(255, 255, 255, 0.12); + border: 1px solid #ffffff; +} diff --git a/internal/web/static/css/cards/cards-scene.css b/internal/web/static/css/cards/cards-scene.css index e8361ec..39639e7 100644 --- a/internal/web/static/css/cards/cards-scene.css +++ b/internal/web/static/css/cards/cards-scene.css @@ -14,3 +14,21 @@ .scene-card .gx-card-tags { margin-top: 0.6rem; } + +/* Harsh pink style reserved for scene cards */ +.scene-card .gx-card { + background: var(--color-brand); + color: #ffffff; + border: 5px solid #ffffff; +} + +.scene-card .gx-card-title, +.scene-card .gx-card-meta, +.scene-card .gx-card-tag { + color: #ffffff; +} + +.scene-card .gx-card-tag { + background: rgba(255, 255, 255, 0.12); + border: 1px solid #ffffff; +} diff --git a/internal/web/static/css/components.css b/internal/web/static/css/components.css index 1dfb068..9ba1d67 100644 --- a/internal/web/static/css/components.css +++ b/internal/web/static/css/components.css @@ -9,16 +9,11 @@ * ============================================ */ .card { background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius); + border: 1px solid var(--color-border); + border-radius: 20px; padding: 1.5rem; - box-shadow: var(--shadow-elevated); - transition: background var(--transition), box-shadow var(--transition); -} - -.card:hover { - background: var(--color-bg-elevated); - box-shadow: var(--shadow-glow-pink-soft); + box-shadow: none; + transition: none; } /* ============================================ @@ -26,26 +21,21 @@ * ============================================ */ .stat-card { background: var(--color-bg-card); - border-radius: var(--radius); + border-radius: 20px; padding: 1.5rem; display: flex; align-items: center; gap: 1.2rem; - border: 1px solid var(--color-border-soft); - box-shadow: var(--shadow-elevated); - transition: transform var(--transition), box-shadow var(--transition); -} - -.stat-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-glow-pink); + border: 1px solid var(--color-border); + box-shadow: none; + transition: none; } .stat-icon { font-size: 2.2rem; color: var(--color-brand); - text-shadow: 0 0 10px var(--color-brand-glow); + text-shadow: none; } .stat-content { @@ -86,9 +76,9 @@ .search-results { margin-top: 0.75rem; background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius); - box-shadow: var(--shadow-elevated); + border: 1px solid var(--color-border); + border-radius: 20px; + box-shadow: none; max-height: 340px; overflow-y: auto; padding: 0.5rem; @@ -96,13 +86,9 @@ .search-result-item { padding: 0.75rem 1rem; - border-radius: var(--radius); + border-radius: 12px; cursor: pointer; - transition: background var(--transition); -} - -.search-result-item:hover { - background: rgba(255, 79, 163, 0.08); + transition: none; } .search-result-title { @@ -227,4 +213,3 @@ transparent ); } - diff --git a/internal/web/static/css/forms.css b/internal/web/static/css/forms.css index 4ce130c..c1af86d 100644 --- a/internal/web/static/css/forms.css +++ b/internal/web/static/css/forms.css @@ -31,17 +31,16 @@ select { width: 100%; padding: 0.9rem 1rem; - background: var(--color-bg-card); + background: var(--color-bg-elevated); color: var(--color-text-primary); - border: 1px solid var(--color-border-soft); + border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 1rem; outline: none; transition: border-color var(--transition), - box-shadow var(--transition), background var(--transition); } @@ -57,8 +56,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--color-brand); - box-shadow: 0 0 0 3px rgba(255, 79, 163, 0.18), - var(--shadow-glow-pink-soft); + box-shadow: none; background: var(--color-bg-elevated); } @@ -96,8 +94,8 @@ input[type="checkbox"] { height: 18px; border-radius: 4px; - border: 1px solid var(--color-border-soft); - background: var(--color-bg-card); + border: 1px solid var(--color-border); + background: var(--color-bg-elevated); cursor: pointer; position: relative; diff --git a/internal/web/static/css/layout.css b/internal/web/static/css/layout.css index e8ecc7a..99b2a43 100644 --- a/internal/web/static/css/layout.css +++ b/internal/web/static/css/layout.css @@ -8,10 +8,7 @@ * =================================== */ body { - background: - radial-gradient(1200px circle at 10% 20%, rgba(255, 79, 163, 0.10), transparent 45%), - radial-gradient(900px circle at 92% 8%, rgba(126, 231, 231, 0.08), transparent 42%), - var(--color-bg-dark); + background: var(--color-bg-dark); min-height: 100vh; } @@ -51,11 +48,10 @@ body { /* Reusable elevated surface */ .surface-panel { - background: linear-gradient(135deg, rgba(255, 79, 163, 0.06), rgba(21, 21, 23, 0.92)); - border: 1px solid var(--color-border-soft); - border-radius: 18px; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 20px; padding: 1.75rem; - box-shadow: var(--shadow-glow-pink-soft); } .section-header { @@ -95,14 +91,12 @@ body { * =================================== */ .navbar { - background: rgba(21, 21, 23, 0.92); - border-bottom: 1px solid var(--color-border-soft); + background: var(--color-bg-card); + border-bottom: 1px solid var(--color-border); padding: 0.85rem 0; position: sticky; top: 0; z-index: 40; - backdrop-filter: blur(8px); - box-shadow: var(--shadow-glow-pink-soft); } .nav-inner { @@ -160,37 +154,18 @@ body { * =================================== */ .hero-section { - background: linear-gradient( - 140deg, - rgba(255, 79, 163, 0.16), - rgba(12, 12, 14, 0.92) - ); - border: 1px solid var(--color-border-soft); + background: var(--color-bg-card); + border: 1px solid var(--color-border); border-radius: 20px; padding: 3rem 2.5rem; margin-bottom: 2rem; position: relative; overflow: hidden; - box-shadow: var(--shadow-glow-pink); display: flex; flex-direction: column; gap: 1.5rem; } -/* Subtle radial neon glow (G-A) */ -.hero-section::after { - content: ""; - position: absolute; - inset: 0; - background: radial-gradient( - circle at 50% 20%, - rgba(255, 79, 163, 0.15), - rgba(255, 79, 163, 0.05) 40%, - transparent 75% - ); - pointer-events: none; -} - .hero-title { font-size: 2.8rem; font-weight: 800; @@ -235,20 +210,14 @@ body { .stat-card { background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius); + border: 1px solid var(--color-border); + border-radius: 20px; padding: 1.4rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; - transition: transform 0.20s var(--transition), - box-shadow 0.20s var(--transition); -} - -.stat-card:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-glow-pink); + transition: none; } .stat-icon { diff --git a/internal/web/static/css/logo-animation.css b/internal/web/static/css/logo-animation.css new file mode 100644 index 0000000..1526e81 --- /dev/null +++ b/internal/web/static/css/logo-animation.css @@ -0,0 +1,28 @@ +/* Minimal bouncing animation for Goondex logo */ +.goondex-logo-animated { + animation: logoBounce 2s ease-in-out infinite; +} + +.goondex-logo-animated .nipple-left, +.goondex-logo-animated .nipple-right { + animation: nippleBounce 2s ease-in-out infinite; +} + +.goondex-logo-animated .nipple-right { + animation-delay: 0.1s; +} + +@keyframes logoBounce { + 0% { transform: translateY(0) scaleY(1); } + 20% { transform: translateY(-20px) scaleY(1.1); } + 30% { transform: translateY(0) scaleY(0.7); } + 40% { transform: translateY(8px) scaleY(1.15); } + 100% { transform: translateY(0) scaleY(1); } +} + +@keyframes nippleBounce { + 0%, 100% { transform: translateY(0); } + 25% { transform: translateY(-6px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-3px); } +} \ No newline at end of file diff --git a/internal/web/static/css/style.css b/internal/web/static/css/style.css index 7ce361e..2088028 100644 --- a/internal/web/static/css/style.css +++ b/internal/web/static/css/style.css @@ -558,6 +558,7 @@ main.container { border: 1px solid var(--color-border); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); display: flex; + flex-direction: column; gap: 1rem; align-items: center; color: var(--color-text-primary); @@ -565,6 +566,19 @@ main.container { justify-content: center; } +.global-loader .logo { + display: flex; + justify-content: center; + margin-bottom: 0.5rem; +} + +.global-loader .logo img, +.global-loader .logo svg { + width: 90px; + height: 55px; + filter: drop-shadow(0 2px 8px rgba(255, 95, 162, 0.3)); +} + .global-loader .spinner { width: 24px; height: 24px; @@ -723,6 +737,17 @@ main.container { text-decoration: underline; } +.btn-link { + color: var(--color-brand); + text-decoration: none; + font-weight: 600; +} + +.btn-link:hover { + color: var(--color-brand-hover); + text-decoration: underline; +} + .full-width { grid-column: 1 / -1; } diff --git a/internal/web/static/css/theme.css b/internal/web/static/css/theme.css index df4fc66..a0297e8 100644 --- a/internal/web/static/css/theme.css +++ b/internal/web/static/css/theme.css @@ -8,28 +8,29 @@ * =========================== */ :root { /* --- BRAND IDENTITY --- */ - --color-brand: #FF4FA3; /* Flamingo Pink (core) */ - --color-brand-hover: #FF6AB7; /* Slightly brighter pink */ - --color-brand-glow: rgba(255, 79, 163, 0.35); /* SUBTLE neon glow */ + --color-brand: #FF4FA3; /* Flamingo Pulse Pink */ + --color-brand-strong: #d74280; /* Deep Flamingo (new) */ + --color-brand-hover: #d74280; /* Hover uses deeper pink */ + --color-brand-glow: transparent; /* Flat theme: no glow */ /* --- TEXT --- */ - --color-text-primary: #F5F5F7; - --color-text-secondary: #A0A3AB; - --color-header: #E08FEA; - --color-keypoint: #FF6ACB; + --color-text-primary: #F8F8F8; + --color-text-secondary: #9BA0A8; + --color-header: #D78BE0; + --color-keypoint: #FF66C4; /* --- ALERTS --- */ --color-warning: #FFAA88; --color-info: #7EE7E7; - /* --- BACKGROUND LAYERS (dark only) --- */ - --color-bg-dark: #0A0A0C; - --color-bg-card: #151517; - --color-bg-elevated: #212124; + /* --- BACKGROUND LAYERS (plum-forward dark) --- */ + --color-bg-dark: #2f2333; /* Plum base */ + --color-bg-card: #3a2b40; /* Card plum */ + --color-bg-elevated: #44344a; /* Elevated plum */ /* --- BORDERS --- */ - --color-border: #3d3d44; - --color-border-soft: rgba(255, 79, 163, 0.15); /* Flamingo soft border */ + --color-border: #59475f; + --color-border-soft: #59475f; /* --- RADII --- */ --radius: 12px; @@ -42,10 +43,10 @@ /* --- UI GRID --- */ --rail-width: 180px; - /* --- GLOWS + SHADOWS (medium intensity only) --- */ - --shadow-glow-pink: 0 0 18px rgba(255, 79, 163, 0.28); - --shadow-glow-pink-soft: 0 0 38px rgba(255, 79, 163, 0.14); - --shadow-elevated: 0 6px 22px rgba(0, 0, 0, 0.6); + /* --- SHADOWS (flattened) --- */ + --shadow-glow-pink: none; + --shadow-glow-pink-soft: none; + --shadow-elevated: none; } /* =========================== @@ -82,12 +83,12 @@ body { ::-webkit-scrollbar-thumb { background: var(--color-brand); border-radius: 6px; - box-shadow: var(--shadow-glow-pink-soft); + box-shadow: none; } ::-webkit-scrollbar-thumb:hover { background: var(--color-brand-hover); - box-shadow: var(--shadow-glow-pink); + box-shadow: none; } /* =========================== @@ -105,22 +106,38 @@ body { /* Subtle glowing border */ .glow-border { border: 1px solid var(--color-border-soft); - box-shadow: var(--shadow-glow-pink-soft); + box-shadow: none; } /* Card elevation */ .elevated { background: var(--color-bg-elevated); - box-shadow: var(--shadow-elevated); + box-shadow: none; } /* Brand glow text (subtle) */ .text-glow { - text-shadow: 0 0 12px var(--color-brand-glow); + text-shadow: none; } /* Pink glow panel (subtle accent for navbar or hero) */ .panel-glow { - box-shadow: inset 0 0 60px rgba(255, 79, 163, 0.08), - 0 0 22px rgba(255, 79, 163, 0.20); + box-shadow: none; +} + +/* Global flat override to strip remaining glow from legacy components */ +body, header, footer, nav, section, article, +.card, .panel, .navbar, .sidebar, .btn, .button, .badge, .chip, .tag, +input, select, textarea, button, +.modal, .dialog, .tooltip, .toast, .dropdown, .tabs, .table { + box-shadow: none !important; + text-shadow: none !important; + filter: none !important; +} + +/* Absolute kill-switch for any remaining glow/shadow */ +*, *::before, *::after { + box-shadow: none !important; + text-shadow: none !important; + filter: none !important; } diff --git a/internal/web/static/img/CSS/performer_info.svg b/internal/web/static/img/CSS/performer_info.svg index 48a36b2..d6148b8 100644 --- a/internal/web/static/img/CSS/performer_info.svg +++ b/internal/web/static/img/CSS/performer_info.svg @@ -27,9 +27,9 @@ inkscape:pagecheckerboard="1" inkscape:deskcolor="#505050" inkscape:document-units="px" - inkscape:zoom="1.4142136" - inkscape:cx="299.10616" - inkscape:cy="244.65894" + inkscape:zoom="2.0000001" + inkscape:cx="205.49999" + inkscape:cy="240.49999" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" @@ -485,7 +485,13 @@ id="tspan49" x="231.02339" y="414.16824" - style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:18.6667px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#262626;fill-opacity:1">8686 el && el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right')); + } + + startBounce() { + if (!this.logoElement || this.isAnimating) return; + this.logoElement.classList.add('goondex-logo-animated'); + this.isAnimating = true; + } + + stopBounce() { + if (!this.logoElement) return; + this.logoElement.classList.remove('goondex-logo-animated'); + this.isAnimating = false; + } +} + +async function loadSVG(urls, targetId) { + const target = document.getElementById(targetId); + if (!target) return null; + for (const url of urls) { + try { + const res = await fetch(url); + if (!res.ok) throw new Error('fetch failed'); + const svgText = await res.text(); + target.innerHTML = svgText; + const svg = target.querySelector('svg'); + return svg; + } catch (e) { + continue; + } + } + // Fallback to img if all fetches fail + target.innerHTML = `Goondex Logo`; + return null; +} + +(async function initLogoAnim() { + const logoURLs = [ + "/static/img/logo/GOONDEX_Titty.svg", + "http://localhost:8788/static/img/logo/GOONDEX_Titty.svg", + ]; + + const staticSvg = await loadSVG(logoURLs, 'static-logo'); + const animatedSvg = await loadSVG(logoURLs, 'animated-logo'); + const loaderSvg = await loadSVG(logoURLs, 'loader-logo'); + + window.goondexLogoAnim = { animator: null, loaderAnimator: null }; + + if (animatedSvg) { + const animator = new LogoAnimator(); + animator.init(animatedSvg); + animator.startBounce(); + window.goondexLogoAnim.animator = animator; + } + if (loaderSvg) { + const l = new LogoAnimator(); + l.init(loaderSvg); + window.goondexLogoAnim.loaderAnimator = l; + } +})(); diff --git a/internal/web/static/js/logo-animation.js b/internal/web/static/js/logo-animation.js new file mode 100644 index 0000000..61d3fd8 --- /dev/null +++ b/internal/web/static/js/logo-animation.js @@ -0,0 +1,58 @@ +// Minimal logo animation controller +class LogoAnimator { + constructor() { + this.isAnimating = false; + this.logoElement = null; + } + + // Initialize with SVG element + init(svgElement) { + this.logoElement = svgElement; + this.identifyNipples(); + } + + // Identify nipple elements by their circular paths + identifyNipples() { + if (!this.logoElement) return; + + const paths = this.logoElement.querySelectorAll('path'); + let nippleIndex = 0; + + paths.forEach((path) => { + const d = path.getAttribute('d'); + // Look for the specific circular nipple paths in the GOONDEX_Titty.svg + if (d && d.includes('1463.5643,67.636337')) { + path.classList.add('nipple-left'); + nippleIndex++; + } else if (d && d.includes('70.4489,0') && nippleIndex === 1) { + path.classList.add('nipple-right'); + nippleIndex++; + } + }); + } + + // Start bouncing animation + startBounce() { + if (!this.logoElement || this.isAnimating) return; + + this.logoElement.classList.add('goondex-logo-animated'); + this.isAnimating = true; + } + + // Stop animation + stopBounce() { + if (!this.logoElement) return; + + this.logoElement.classList.remove('goondex-logo-animated'); + this.isAnimating = false; + } + + // Auto-start for loading screens + autoStart(duration = 3000) { + this.startBounce(); + setTimeout(() => this.stopBounce(), duration); + } +} + +// Export for use in loading screens +window.LogoAnimator = LogoAnimator; \ No newline at end of file diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index 1efc6d7..207a307 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -15,7 +15,7 @@

Full-library sync with seamless enrichment

- @@ -56,7 +56,7 @@
- diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index 43b4f09..edc4050 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -59,6 +59,9 @@ diff --git a/scripts/config/api_keys.json b/scripts/config/api_keys.json new file mode 100644 index 0000000..8ee5be5 --- /dev/null +++ b/scripts/config/api_keys.json @@ -0,0 +1,6 @@ +{ + "tpdb_api_key": "Dn8q3mdZd7mE4OHUqf7k1A3q813i48t7q1418zv87c477738", + "ae_api_key": "", + "stashdb_api_key": "", + "stashdb_endpoint": "https://stashdb.org/graphql" +} \ No newline at end of file diff --git a/scripts/enrich.sh b/scripts/enrich.sh new file mode 100644 index 0000000..7f8719c --- /dev/null +++ b/scripts/enrich.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Enrichment helper (Adult Empire enricher) +# Usage: +# ./scripts/enrich.sh all +# ./scripts/enrich.sh performers +# ./scripts/enrich.sh scenes +# Optional flags are passed through after the subcommand, e.g.: +# ./scripts/enrich.sh performers --start-id 100 --limit 50 + +set -euo pipefail + +cmd="${1:-}" +shift || true + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +run() { + echo "▶ $*" + if [[ -x "$repo_root/goondex" ]]; then + exec "$repo_root/goondex" "$@" + elif [[ -x "$repo_root/bin/goondex" ]]; then + exec "$repo_root/bin/goondex" "$@" + else + echo "goondex binary not found. Build it first with: go build -o bin/goondex ./cmd/goondex" >&2 + exit 1 + fi +} + +case "$cmd" in + all) + run enrich all-performers "$@" + ;; + performers|performer) + run enrich all-performers "$@" + ;; + scenes|scene) + run enrich all-scenes "$@" + ;; + *) + cat <<'EOF' >&2 +Usage: ./scripts/enrich.sh {all|performers|scenes} [flags] + +Examples: + ./scripts/enrich.sh all + ./scripts/enrich.sh performers --start-id 100 --limit 50 + ./scripts/enrich.sh scenes --start-id 200 +EOF + exit 1 + ;; +esac diff --git a/scripts/run.sh b/scripts/run.sh index 00208c0..859309a 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -6,6 +6,16 @@ source "$ROOT/scripts/env.sh" ADDR="${ADDR:-localhost:8788}" +# Auto-stop if already running on the same port +if command -v lsof >/dev/null 2>&1; then + pids=$(lsof -t -i "@${ADDR#*:}:${ADDR##*:}" 2>/dev/null) + if [[ -n "$pids" ]]; then + echo "Stopping existing goondex on $ADDR (pids: $pids)" + kill $pids 2>/dev/null || true + sleep 0.5 + fi +fi + # Build if missing if [[ ! -x "$ROOT/bin/goondex" ]]; then echo "Binary not found; building first..." diff --git a/scripts/set_api_key.sh b/scripts/set_api_key.sh new file mode 100755 index 0000000..30d1e03 --- /dev/null +++ b/scripts/set_api_key.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Persist TPDB (and optional AE/Stash) API keys to config/api_keys.json +# Usage: +# ./scripts/set_api_key.sh [ae-key] [stashdb-key] +# +# This writes config/api_keys.json (gitignored) and echoes an export line +# you can paste to set the env var for the current shell if desired. + +set -euo pipefail + +tpdb="${1:-}" +ae="${2:-}" +stash="${3:-}" + +if [[ -z "$tpdb" ]]; then + echo "Usage: $0 [ae-key] [stashdb-key]" >&2 + exit 1 +fi + +python - <<'PY' "$tpdb" "$ae" "$stash" +import json, sys, os + +tpdb, ae, stash = sys.argv[1], sys.argv[2] or None, sys.argv[3] or None +path = os.path.join("config", "api_keys.json") +data = {} +if os.path.exists(path): + try: + with open(path, "r") as f: + data = json.load(f) + except Exception: + data = {} + +data["tpdb_api_key"] = tpdb +if ae: + data["ae_api_key"] = ae +if stash: + data["stashdb_api_key"] = stash + +os.makedirs(os.path.dirname(path), exist_ok=True) +with open(path, "w") as f: + json.dump(data, f, indent=2) + +print(f"Wrote {path}") +print(f'TPDB key set: {tpdb[:4]}... (hidden)') +PY + +echo "To set the env var for this shell, run:" +echo " export TPDB_API_KEY=\"${tpdb}\"" diff --git a/scripts/status.sh b/scripts/status.sh new file mode 100755 index 0000000..4ead9b9 --- /dev/null +++ b/scripts/status.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Goondex status snapshot +# Usage: ./scripts/status.sh + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +# Check binary +if [[ -x "$repo_root/goondex" ]]; then + bin="$repo_root/goondex" +elif [[ -x "$repo_root/bin/goondex" ]]; then + bin="$repo_root/bin/goondex" +else + bin="" +fi + +# DB info (file size) +db_path="$repo_root/goondex.db" +db_size="missing" +if [[ -f "$db_path" ]]; then + db_size=$(du -h "$db_path" | awk '{print $1}') +fi + +# API key presence +keys_file="$repo_root/config/api_keys.json" +tpdb_key="missing" +if [[ -f "$keys_file" ]]; then + tpdb_key=$(python - <<'PY' "$keys_file" +import json,sys +try: + with open(sys.argv[1]) as f: + data=json.load(f) + key=data.get("tpdb_api_key") + print("set" if key else "missing") +except Exception: + print("missing") +PY +) +fi + +# Basic counts (if sqlite3 is available) +scene_count="n/a"; performer_count="n/a"; studio_count="n/a"; movie_count="n/a" +if command -v sqlite3 >/dev/null 2>&1 && [[ -f "$db_path" ]]; then + scene_count=$(sqlite3 "$db_path" 'select count(*) from scenes;') || scene_count="err" + performer_count=$(sqlite3 "$db_path" 'select count(*) from performers;') || performer_count="err" + studio_count=$(sqlite3 "$db_path" 'select count(*) from studios;') || studio_count="err" + movie_count=$(sqlite3 "$db_path" 'select count(*) from movies;') || movie_count="err" +fi + +# Status summary +cat </dev/null 2>&1; then + echo "Git:" $(git status --porcelain | wc -l) "dirty file(s)" +fi diff --git a/scripts/tpdb_import.sh b/scripts/tpdb_import.sh new file mode 100755 index 0000000..5bcd83f --- /dev/null +++ b/scripts/tpdb_import.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# TPDB import helper (TUI-friendly runner) +# Usage: +# ./scripts/tpdb_import.sh all +# ./scripts/tpdb_import.sh performers +# ./scripts/tpdb_import.sh studios +# ./scripts/tpdb_import.sh scenes + +set -euo pipefail + +cmd="${1:-}" + +# Try env first, then config/api_keys.json +if [[ -z "${TPDB_API_KEY:-}" ]]; then + if [[ -f "../config/api_keys.json" ]]; then + TPDB_API_KEY="$( +python - <<'PY' "../config/api_keys.json" +import json, sys +p = sys.argv[1] +try: + with open(p) as f: + data = json.load(f) + print(data.get("tpdb_api_key", "")) +except Exception: + print("") +PY + )" + fi +fi + +if [[ -z "${TPDB_API_KEY:-}" ]]; then + echo "TPDB_API_KEY is not set. Export it, or save it via scripts/set_api_key.sh." >&2 + echo ' export TPDB_API_KEY="your-key-here"' >&2 + exit 1 +fi + +run() { + echo "▶ $*" + repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + if [[ -x "$repo_root/goondex" ]]; then + exec "$repo_root/goondex" "$@" + elif [[ -x "$repo_root/bin/goondex" ]]; then + exec "$repo_root/bin/goondex" "$@" + else + echo "goondex binary not found. Build it first with: go build -o bin/goondex ./cmd/goondex" >&2 + exit 1 + fi +} + +case "$cmd" in + all) + run import all + ;; + performers|performer) + run import performer + ;; + studios|studio) + run import studio + ;; + scenes|scene) + run import scene + ;; + *) + cat <<'EOF' >&2 +Usage: ./scripts/tpdb_import.sh {all|performers|studios|scenes} + +Examples: + ./scripts/tpdb_import.sh all + ./scripts/tpdb_import.sh performers + ./scripts/tpdb_import.sh studios + ./scripts/tpdb_import.sh scenes +EOF + exit 1 + ;; +esac diff --git a/test-logo-standalone.html b/test-logo-standalone.html new file mode 100644 index 0000000..b33e7c0 --- /dev/null +++ b/test-logo-standalone.html @@ -0,0 +1,310 @@ + + + + Logo Animation Test + + + +

Goondex Logo Animation Test

+ +
+

Static Logo:

+ +
+ +
+

Animated Logo:

+ +
+ +
+ + +
+ +
+ +
+ + + + + + diff --git a/test-logo.html b/test-logo.html new file mode 100644 index 0000000..53c02f0 --- /dev/null +++ b/test-logo.html @@ -0,0 +1,64 @@ + + + + Logo Animation Test + + + + + +

Goondex Logo Animation Test

+ +
+

Static Logo:

+ +
+ +
+

Animated Logo:

+ +
+ +
+ + +
+ +
+

Loader Test:

+ +
+ + + + + + \ No newline at end of file