| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package json // import "miniflux.app/v2/internal/reader/json"
- import (
- "log/slog"
- "slices"
- "strings"
- "time"
- "miniflux.app/v2/internal/crypto"
- "miniflux.app/v2/internal/model"
- "miniflux.app/v2/internal/reader/date"
- "miniflux.app/v2/internal/reader/sanitizer"
- "miniflux.app/v2/internal/urllib"
- )
- type JSONAdapter struct {
- jsonFeed *JSONFeed
- }
- func NewJSONAdapter(jsonFeed *JSONFeed) *JSONAdapter {
- return &JSONAdapter{jsonFeed}
- }
- func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
- feed := &model.Feed{
- Title: strings.TrimSpace(j.jsonFeed.Title),
- FeedURL: strings.TrimSpace(j.jsonFeed.FeedURL),
- SiteURL: strings.TrimSpace(j.jsonFeed.HomePageURL),
- Description: strings.TrimSpace(j.jsonFeed.Description),
- }
- if feed.FeedURL == "" {
- feed.FeedURL = strings.TrimSpace(baseURL)
- }
- // Fallback to the feed URL if the site URL is empty.
- if feed.SiteURL == "" {
- feed.SiteURL = feed.FeedURL
- }
- if feedURL, err := urllib.ResolveToAbsoluteURL(baseURL, feed.FeedURL); err == nil {
- feed.FeedURL = feedURL
- }
- if siteURL, err := urllib.ResolveToAbsoluteURL(baseURL, feed.SiteURL); err == nil {
- feed.SiteURL = siteURL
- }
- // Fallback to the feed URL if the title is empty.
- if feed.Title == "" {
- feed.Title = feed.SiteURL
- }
- // Populate the icon URL if present.
- for _, iconURL := range []string{j.jsonFeed.FaviconURL, j.jsonFeed.IconURL} {
- iconURL = strings.TrimSpace(iconURL)
- if iconURL != "" {
- if absoluteIconURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, iconURL); err == nil {
- feed.IconURL = absoluteIconURL
- break
- }
- }
- }
- for _, item := range j.jsonFeed.Items {
- entry := model.NewEntry()
- entry.Title = strings.TrimSpace(item.Title)
- for _, itemURL := range []string{item.URL, item.ExternalURL} {
- itemURL = strings.TrimSpace(itemURL)
- if itemURL != "" {
- // Make sure the entry URL is absolute.
- if entryURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, itemURL); err == nil {
- entry.URL = entryURL
- }
- break
- }
- }
- // The entry title is optional, so we need to find a fallback.
- if entry.Title == "" {
- for _, value := range []string{item.Summary, item.ContentText, item.ContentHTML} {
- value = strings.TrimSpace(value)
- if value != "" {
- entry.Title = sanitizer.TruncateHTML(value, 100)
- break
- }
- }
- }
- // Fallback to the entry URL if the title is empty.
- if entry.Title == "" {
- entry.Title = entry.URL
- }
- // Populate the entry content.
- for _, value := range []string{item.ContentHTML, item.ContentText, item.Summary} {
- value = strings.TrimSpace(value)
- if value != "" {
- entry.Content = value
- break
- }
- }
- // Populate the entry date.
- for _, value := range []string{item.DatePublished, item.DateModified} {
- value = strings.TrimSpace(value)
- if value != "" {
- if date, err := date.Parse(value); err != nil {
- slog.Debug("Unable to parse date from JSON feed",
- slog.String("date", value),
- slog.String("url", entry.URL),
- slog.Any("error", err),
- )
- } else {
- entry.Date = date
- break
- }
- }
- }
- if entry.Date.IsZero() {
- entry.Date = time.Now()
- }
- // Populate the entry author.
- itemAuthors := j.jsonFeed.Authors
- itemAuthors = append(itemAuthors, item.Authors...)
- itemAuthors = append(itemAuthors, item.Author, j.jsonFeed.Author)
- var authorNames []string
- for _, author := range itemAuthors {
- authorName := strings.TrimSpace(author.Name)
- if authorName != "" {
- authorNames = append(authorNames, authorName)
- }
- }
- slices.Sort(authorNames)
- authorNames = slices.Compact(authorNames)
- entry.Author = strings.Join(authorNames, ", ")
- // Populate the entry enclosures.
- for _, attachment := range item.Attachments {
- attachmentURL := strings.TrimSpace(attachment.URL)
- if attachmentURL != "" {
- if absoluteAttachmentURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, attachmentURL); err == nil {
- entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
- URL: absoluteAttachmentURL,
- MimeType: attachment.MimeType,
- Size: attachment.Size,
- })
- }
- }
- }
- // Populate the entry tags.
- for _, tag := range item.Tags {
- tag = strings.TrimSpace(tag)
- if tag != "" {
- entry.Tags = append(entry.Tags, tag)
- }
- }
- // Sort and deduplicate tags.
- slices.Sort(entry.Tags)
- entry.Tags = slices.Compact(entry.Tags)
- // Generate a hash for the entry.
- for _, value := range []string{item.ID, item.URL, item.ExternalURL, item.ContentText + item.ContentHTML + item.Summary} {
- value = strings.TrimSpace(value)
- if value != "" {
- entry.Hash = crypto.SHA256(value)
- break
- }
- }
- feed.Entries = append(feed.Entries, entry)
- }
- return feed
- }
|