feat: implement comic/manga metadata display on book detail page
Implement comprehensive comic metadata display features on the SSR book detail page, supporting all 8 metadata fields from ComicInfo.xml and other sources. ## Template Changes (book_detail.templ) ### Step 1: Reading Direction Badge - Display directional badge (RTL, LTR, VERTICAL) for manga/comics - Uses 📖 icon with uppercase direction text - Auto-hides when direction is "auto" (default) - Styled with accent color for visibility ### Step 2: Community Rating Display - Show pre-existing community rating from metadata (0.0-10.0 scale) - Distinct from user ratings with visual differentiation - Uses renderStars() helper for visual star display - Shows both stars and numeric score (e.g., "★★★★☆ 8.5 / 10") - Smaller, subtler styling than user rating ### Step 3: Comic-Specific Badges - Age Rating: Content maturity indicator - Black & White: Visual style badge - Story Arc: Narrative arc name with 📚 icon - Badges styled as pills with subtle borders - Only display when values are present ### Step 4: Universal Series Info - Series Count: Total items in series - Volume: Volume/omnibus number - Imprint: Publisher imprint (e.g., Vertigo) ### Step 5: Comic-Specific Metadata - Manga Type: Raw/Comic/Manga classification - Scan Information: Scanner group, resolution - Alternate Series: Different series numbering ### Step 6: Summary Section - Display ComicInfo.xml summary when present - Separate from description field - Sanitized HTML output with bluemonday - Scrollable container for long summaries ### Step 7: Metadata Notes - Technical notes from metadata files - Internal/useful information (scanner, source, etc.) - Card-style display with clear typography ### Step 8: Web URL Link - External link to info sources (Goodreads, ComicVine, etc.) - Opens in new tab with security attributes - Displays clean domain name ## Utils Changes (templates/utils.go) Added helper functions: - getAlternateSeries(): Extract alternate series from JSONB - getDomainName(): Extract clean domain for display - formatAlternateInfo(): Format readable alternate series string ## Implementation Plan Updated FRONTEND_IMPLEMENTATION_PLAN_COMIC_METADATA.md with: - Disabled markdownlint for MD013 (line length) - Added spacing for readability ## Technical Details - All fields use pgtype.Text/Int4/Bool for NULL handling - Template conditionals check Valid flag before accessing values - Consistent styling using CSS custom properties - HTML escaping for security (except summary with bluemonday) - Responsive design with mobile-friendly layouts Related: Database schema already supports all metadata fields
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- markdownlint-disable MD013 -->
|
||||||
|
|
||||||
# Frontend Implementation Plan: Add Comic Metadata Fields to Book Detail Page
|
# Frontend Implementation Plan: Add Comic Metadata Fields to Book Detail Page
|
||||||
|
|
||||||
**Status:** Planning Document (Not Implemented)
|
**Status:** Planning Document (Not Implemented)
|
||||||
@@ -13,6 +15,7 @@ This document outlines how to add the new ComicInfo.xml metadata fields to the b
|
|||||||
## New Fields to Display
|
## New Fields to Display
|
||||||
|
|
||||||
### Universal Fields (apply to all formats: ebooks, audiobooks, comics)
|
### Universal Fields (apply to all formats: ebooks, audiobooks, comics)
|
||||||
|
|
||||||
- `series_count` - Total items in series
|
- `series_count` - Total items in series
|
||||||
- `volume` - Volume/omnibus number
|
- `volume` - Volume/omnibus number
|
||||||
- `imprint` - Publisher imprint (e.g., Vertigo, HarperCollinsEpic)
|
- `imprint` - Publisher imprint (e.g., Vertigo, HarperCollinsEpic)
|
||||||
@@ -22,6 +25,7 @@ This document outlines how to add the new ComicInfo.xml metadata fields to the b
|
|||||||
- `community_rating` - Pre-existing community rating (0.0-10.0) - **distinct from user ratings**
|
- `community_rating` - Pre-existing community rating (0.0-10.0) - **distinct from user ratings**
|
||||||
|
|
||||||
### Comic-Specific Fields
|
### Comic-Specific Fields
|
||||||
|
|
||||||
- `manga_type` - Raw Manga field: unknown, no, yes, yes_and_right_to_left
|
- `manga_type` - Raw Manga field: unknown, no, yes, yes_and_right_to_left
|
||||||
- `reading_direction` - Computed reading direction: auto, ltr, rtl, vertical
|
- `reading_direction` - Computed reading direction: auto, ltr, rtl, vertical
|
||||||
- `story_arc` - Story arc name (e.g., "The Dark Phoenix Saga", "Civil War")
|
- `story_arc` - Story arc name (e.g., "The Dark Phoenix Saga", "Civil War")
|
||||||
@@ -36,7 +40,7 @@ This document outlines how to add the new ComicInfo.xml metadata fields to the b
|
|||||||
|
|
||||||
**File:** `templates/book_detail.templ`
|
**File:** `templates/book_detail.templ`
|
||||||
|
|
||||||
### Current Layout Sections:
|
### Current Layout Sections
|
||||||
|
|
||||||
1. **Top Section** (lines 23-130)
|
1. **Top Section** (lines 23-130)
|
||||||
- Cover image (left)
|
- Cover image (left)
|
||||||
@@ -81,6 +85,7 @@ if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.Rea
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Styling Notes:**
|
**Styling Notes:**
|
||||||
|
|
||||||
- Use `var(--accent)` for consistency with series badge
|
- Use `var(--accent)` for consistency with series badge
|
||||||
- Only show if not "auto" (i.e., explicitly set to ltr, rtl, or vertical)
|
- Only show if not "auto" (i.e., explicitly set to ltr, rtl, or vertical)
|
||||||
- Icon: 📖 for visual clarity
|
- Icon: 📖 for visual clarity
|
||||||
@@ -111,6 +116,7 @@ if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Styling Notes:**
|
**Styling Notes:**
|
||||||
|
|
||||||
- Smaller text than user rating (text-lg vs text-2xl)
|
- Smaller text than user rating (text-lg vs text-2xl)
|
||||||
- Gray/subtle color to distinguish from user rating
|
- Gray/subtle color to distinguish from user rating
|
||||||
- Scale: Community rating is 0-10, user rating display is 0-5 stars
|
- Scale: Community rating is 0-10, user rating display is 0-5 stars
|
||||||
@@ -160,6 +166,7 @@ if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Styling Notes:**
|
**Styling Notes:**
|
||||||
|
|
||||||
- Use subtle styling (smaller, outlined) to differentiate from primary badges
|
- Use subtle styling (smaller, outlined) to differentiate from primary badges
|
||||||
- Flex wrap for responsive layout
|
- Flex wrap for responsive layout
|
||||||
- Icons: 📚 for story arc
|
- Icons: 📚 for story arc
|
||||||
@@ -225,6 +232,7 @@ if book.AlternateInfo.Valid && len(book.AlternateInfo.Bytes) > 0 {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Note for AlternateInfo:** This is JSONB data stored as `[]byte`. You may want to:
|
**Note for AlternateInfo:** This is JSONB data stored as `[]byte`. You may want to:
|
||||||
|
|
||||||
1. Parse the JSON and display formatted
|
1. Parse the JSON and display formatted
|
||||||
2. Or create a helper function to extract specific fields
|
2. Or create a helper function to extract specific fields
|
||||||
|
|
||||||
@@ -248,6 +256,7 @@ func getAlternateSeries(data pgtype.Bytes) string {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Then in template:
|
Then in template:
|
||||||
|
|
||||||
```templ
|
```templ
|
||||||
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
||||||
<div>
|
<div>
|
||||||
@@ -448,26 +457,34 @@ func formatAlternateInfo(data pgtype.Bytes) string {
|
|||||||
## Testing Scenarios
|
## Testing Scenarios
|
||||||
|
|
||||||
### Test Case 1: Japanese Manga
|
### Test Case 1: Japanese Manga
|
||||||
|
|
||||||
**Expected Display:**
|
**Expected Display:**
|
||||||
|
|
||||||
- Reading Direction: "RTL" badge
|
- Reading Direction: "RTL" badge
|
||||||
- Manga Type: "yes_and_right_to_left" in metadata grid
|
- Manga Type: "yes_and_right_to_left" in metadata grid
|
||||||
- Community Rating: displayed alongside user rating
|
- Community Rating: displayed alongside user rating
|
||||||
|
|
||||||
### Test Case 2: Western Comic
|
### Test Case 2: Western Comic
|
||||||
|
|
||||||
**Expected Display:**
|
**Expected Display:**
|
||||||
|
|
||||||
- Reading Direction: "LTR" badge (or hidden if auto)
|
- Reading Direction: "LTR" badge (or hidden if auto)
|
||||||
- Age Rating: "Teen" or "Mature" badge
|
- Age Rating: "Teen" or "Mature" badge
|
||||||
- Story Arc: displayed if present
|
- Story Arc: displayed if present
|
||||||
- Publisher/Imprint: displayed in metadata grid
|
- Publisher/Imprint: displayed in metadata grid
|
||||||
|
|
||||||
### Test Case 3: Webtoon/Manhwa
|
### Test Case 3: Webtoon/Manhwa
|
||||||
|
|
||||||
**Expected Display:**
|
**Expected Display:**
|
||||||
|
|
||||||
- Reading Direction: "vertical" badge
|
- Reading Direction: "vertical" badge
|
||||||
- Language: "ko" (Korean) in metadata grid
|
- Language: "ko" (Korean) in metadata grid
|
||||||
- Format-specific metadata displayed
|
- Format-specific metadata displayed
|
||||||
|
|
||||||
### Test Case 4: Regular Ebook
|
### Test Case 4: Regular Ebook
|
||||||
|
|
||||||
**Expected Display:**
|
**Expected Display:**
|
||||||
|
|
||||||
- No reading direction badge (or "auto" hidden)
|
- No reading direction badge (or "auto" hidden)
|
||||||
- No comic-specific fields
|
- No comic-specific fields
|
||||||
- Only universal fields (series count, volume, imprint, age rating) if present
|
- Only universal fields (series count, volume, imprint, age rating) if present
|
||||||
|
|||||||
+135
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"bookhoard/internal/handlers"
|
"bookhoard/internal/handlers"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
||||||
@@ -101,6 +102,20 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
({ fmt.Sprintf("%.1f", float64(getBookRating(book.Rating))/2.0) } / 5)
|
({ fmt.Sprintf("%.1f", float64(getBookRating(book.Rating))/2.0) } / 5)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Community Rating Display (from metadata) -->
|
||||||
|
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="text-lg" style="color: var(--text-secondary);">
|
||||||
|
Community Rating:
|
||||||
|
<span class="font-bold" style="color: var(--text-primary);">
|
||||||
|
@templ.Raw(renderStars(int32(book.CommunityRating.Float64)))
|
||||||
|
</span>
|
||||||
|
<span class="ml-2 text-sm" style="color: var(--text-secondary);">
|
||||||
|
{ fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) }
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<!-- Series Badge -->
|
<!-- Series Badge -->
|
||||||
if book.Series.Valid && book.Series.String != "" {
|
if book.Series.Valid && book.Series.String != "" {
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -115,6 +130,47 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Reading Direction Badge (for manga/comics) -->
|
||||||
|
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
|
||||||
|
<div class="mb-4">
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full text-sm font-semibold"
|
||||||
|
style="background-color: var(--accent); color: white;"
|
||||||
|
>
|
||||||
|
📖 { strings.ToUpper(book.ReadingDirection.String) }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<!-- Comic-Specific Badges -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
<!-- Age Rating Badge -->
|
||||||
|
if book.AgeRating.Valid && book.AgeRating.String != "" {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
{ book.AgeRating.String }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<!-- Black and White Badge -->
|
||||||
|
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
B&W
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<!-- Story Arc Badge -->
|
||||||
|
if book.StoryArc.Valid && book.StoryArc.String != "" {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
📚 { book.StoryArc.String }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<!-- Description/Synopsis -->
|
<!-- Description/Synopsis -->
|
||||||
if book.Description.Valid && book.Description.String != "" {
|
if book.Description.Valid && book.Description.String != "" {
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@@ -128,6 +184,29 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Summary (from ComicInfo.xml, if different from description) -->
|
||||||
|
if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String {
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Comic Summary</h3>
|
||||||
|
<div class="max-h-[20rem] overflow-y-auto pr-2" style="color: var(--text-secondary)">
|
||||||
|
@UnsafeHTML(
|
||||||
|
bluemonday.UGCPolicy().Sanitize(book.Summary.String),
|
||||||
|
).ToComponent()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<!-- Metadata Notes (technical notes from metadata files) -->
|
||||||
|
if book.MetadataNotes.Valid && book.MetadataNotes.String != "" {
|
||||||
|
<div
|
||||||
|
class="card p-6 rounded-lg border mb-6"
|
||||||
|
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Metadata Notes</h3>
|
||||||
|
<div class="text-sm" style="color: var(--text-secondary);">
|
||||||
|
{ book.MetadataNotes.String }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<!-- Progress Section -->
|
<!-- Progress Section -->
|
||||||
if book.ReadingProgress != nil {
|
if book.ReadingProgress != nil {
|
||||||
<div
|
<div
|
||||||
@@ -233,12 +312,56 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
<p>{ book.Genre.String }</p>
|
<p>{ book.Genre.String }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Series Information (if not shown in badge) -->
|
||||||
|
if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Series Count</p>
|
||||||
|
<p>{ book.SeriesCount.Int32 } items</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.Volume.Valid && book.Volume.Int32 > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Volume</p>
|
||||||
|
<p>Vol. { book.Volume.Int32 }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.Imprint.Valid && book.Imprint.String != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Imprint</p>
|
||||||
|
<p>{ book.Imprint.String }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 {
|
if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 {
|
||||||
<div>
|
<div>
|
||||||
<p style="color: var(--text-secondary)">Copyright Year</p>
|
<p style="color: var(--text-secondary)">Copyright Year</p>
|
||||||
<p>{ book.CopyrightYear.Int32 }</p>
|
<p>{ book.CopyrightYear.Int32 }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Comic-Specific Fields -->
|
||||||
|
if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Manga Type</p>
|
||||||
|
<p class="capitalize">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.ScanInformation.Valid && book.ScanInformation.String != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Scan Info</p>
|
||||||
|
<p class="text-sm" style="color: var(--text-secondary);">{ book.ScanInformation.String }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if len(book.AlternateInfo) > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||||
|
<p class="text-sm">{ string(book.AlternateInfo) }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||||
|
<p>{ altSeries }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<!-- Technical Info -->
|
<!-- Technical Info -->
|
||||||
<div>
|
<div>
|
||||||
<p style="color: var(--text-secondary)">Format</p>
|
<p style="color: var(--text-secondary)">Format</p>
|
||||||
@@ -252,7 +375,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<!-- External IDs Section -->
|
<!-- External IDs Section -->
|
||||||
if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid {
|
if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid || book.WebUrl.Valid {
|
||||||
<div class="mt-4 pt-4 border-t" style="border-color: var(--border);">
|
<div class="mt-4 pt-4 border-t" style="border-color: var(--border);">
|
||||||
<h4 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">External Links</h4>
|
<h4 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">External Links</h4>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
@@ -340,6 +463,17 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|||||||
🛒 Amazon
|
🛒 Amazon
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
if book.WebUrl.Valid && book.WebUrl.String != "" {
|
||||||
|
<a
|
||||||
|
href={ book.WebUrl.String }
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm hover:underline flex items-center gap-1"
|
||||||
|
style="color: var(--accent);"
|
||||||
|
>
|
||||||
|
🔗 { getDomainName(book.WebUrl.String) }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
+637
-310
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@ package templates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -142,3 +144,56 @@ func getBookRating(rating *database.MediaRatings) int32 {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAlternateSeries extracts the alternate series name from JSONB data
|
||||||
|
func getAlternateSeries(data []byte) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if series, ok := result["alternate_series"].(string); ok {
|
||||||
|
return series
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDomainName extracts a clean domain name from URL for display
|
||||||
|
func getDomainName(rawURL string) string {
|
||||||
|
if rawURL == "" {
|
||||||
|
return "Source"
|
||||||
|
}
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "Source"
|
||||||
|
}
|
||||||
|
// Return hostname without www.
|
||||||
|
hostname := u.Hostname()
|
||||||
|
return strings.TrimPrefix(hostname, "www.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatAlternateInfo formats alternate series info as readable string
|
||||||
|
func formatAlternateInfo(data []byte) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
if series, ok := result["alternate_series"].(string); ok && series != "" {
|
||||||
|
parts = append(parts, series)
|
||||||
|
}
|
||||||
|
if num, ok := result["alternate_number"].(float64); ok && num > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("#%d", int(num)))
|
||||||
|
}
|
||||||
|
if count, ok := result["alternate_count"].(float64); ok && count > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("(of %d)", int(count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user