Parcourir la source

Add description field to feed settings

This adds a new "description" field to the feed settings. This allows to
save custom description regarding a feed. It is also exported and
imported as "description" in OPML.
Jan-Lukas Else il y a 1 an
Parent
commit
a33b1adf13
33 fichiers modifiés avec 72 ajouts et 20 suppressions
  1. 5 0
      internal/database/migrations.go
  2. 1 0
      internal/locale/translations/de_DE.json
  3. 1 0
      internal/locale/translations/el_EL.json
  4. 1 0
      internal/locale/translations/en_US.json
  5. 1 0
      internal/locale/translations/es_ES.json
  6. 1 0
      internal/locale/translations/fi_FI.json
  7. 1 0
      internal/locale/translations/fr_FR.json
  8. 1 0
      internal/locale/translations/hi_IN.json
  9. 1 0
      internal/locale/translations/id_ID.json
  10. 1 0
      internal/locale/translations/it_IT.json
  11. 1 0
      internal/locale/translations/ja_JP.json
  12. 1 0
      internal/locale/translations/nl_NL.json
  13. 1 0
      internal/locale/translations/pl_PL.json
  14. 1 0
      internal/locale/translations/pt_BR.json
  15. 1 0
      internal/locale/translations/ru_RU.json
  16. 1 0
      internal/locale/translations/tr_TR.json
  17. 1 0
      internal/locale/translations/uk_UA.json
  18. 1 0
      internal/locale/translations/zh_CN.json
  19. 1 0
      internal/locale/translations/zh_TW.json
  20. 6 0
      internal/model/feed.go
  21. 7 5
      internal/reader/opml/handler.go
  22. 6 5
      internal/reader/opml/opml.go
  23. 1 0
      internal/reader/opml/parser.go
  24. 1 1
      internal/reader/opml/parser_test.go
  25. 5 4
      internal/reader/opml/serializer.go
  26. 3 1
      internal/reader/opml/subscription.go
  27. 2 0
      internal/storage/entry_query_builder.go
  28. 8 4
      internal/storage/feed.go
  29. 2 0
      internal/storage/feed_query_builder.go
  30. 3 0
      internal/template/templates/views/edit_feed.html
  31. 1 0
      internal/ui/feed_edit.go
  32. 1 0
      internal/ui/feed_update.go
  33. 3 0
      internal/ui/form/feed.go

+ 5 - 0
internal/database/migrations.go

@@ -898,4 +898,9 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `ALTER TABLE feeds ADD COLUMN description text default ''`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 1 - 0
internal/locale/translations/de_DE.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Titel",
     "form.feed.label.site_url": "URL der Webseite",
     "form.feed.label.feed_url": "URL des Abonnements",
+    "form.feed.label.description": "Beschreibung",
     "form.feed.label.category": "Kategorie",
     "form.feed.label.crawler": "Originalinhalt herunterladen",
     "form.feed.label.feed_username": "Benutzername des Abonnements",

+ 1 - 0
internal/locale/translations/el_EL.json

@@ -319,6 +319,7 @@
     "form.feed.label.title": "Τίτλος",
     "form.feed.label.site_url": "Διεύθυνση URL ιστότοπου",
     "form.feed.label.feed_url": "Διεύθυνση URL ροής",
+    "form.feed.label.description": "Περιγραφή",
     "form.feed.label.category": "Κατηγορία",
     "form.feed.label.crawler": "Λήψη αρχικού περιεχομένου",
     "form.feed.label.feed_username": "Όνομα Χρήστη ροής",

+ 1 - 0
internal/locale/translations/en_US.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Title",
     "form.feed.label.site_url": "Site URL",
     "form.feed.label.feed_url": "Feed URL",
+    "form.feed.label.description": "Description",
     "form.feed.label.category": "Category",
     "form.feed.label.crawler": "Fetch original content",
     "form.feed.label.feed_username": "Feed Username",

+ 1 - 0
internal/locale/translations/es_ES.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Título",
     "form.feed.label.site_url": "URL del sitio",
     "form.feed.label.feed_url": "URL de la fuente",
+    "form.feed.label.description": "Descripción",
     "form.feed.label.category": "Categoría",
     "form.feed.label.crawler": "Obtener rastreador original",
     "form.feed.label.feed_username": "Nombre de usuario de la fuente",

+ 1 - 0
internal/locale/translations/fi_FI.json

@@ -319,6 +319,7 @@
     "form.feed.label.title": "Otsikko",
     "form.feed.label.site_url": "Sivuston URL-osoite",
     "form.feed.label.feed_url": "Syötteen URL-osoite",
+    "form.feed.label.description": "Kuvaus",
     "form.feed.label.category": "Kategoria",
     "form.feed.label.crawler": "Nouda alkuperäinen sisältö",
     "form.feed.label.feed_username": "Syötteen käyttäjätunnus",

+ 1 - 0
internal/locale/translations/fr_FR.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Titre",
     "form.feed.label.site_url": "URL du site web",
     "form.feed.label.feed_url": "URL du flux",
+    "form.feed.label.description": "Description",
     "form.feed.label.category": "Catégorie",
     "form.feed.label.crawler": "Récupérer le contenu original",
     "form.feed.label.feed_username": "Nom d'utilisateur du flux",

+ 1 - 0
internal/locale/translations/hi_IN.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "शीर्षक",
     "form.feed.label.site_url": "साइट यूआरएल",
     "form.feed.label.feed_url": "फ़ीड यूआरएल",
+    "form.feed.label.description": "विवरण",
     "form.feed.label.category": "श्रेणी",
     "form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
     "form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",

+ 1 - 0
internal/locale/translations/id_ID.json

@@ -307,6 +307,7 @@
     "form.feed.label.title": "Judul",
     "form.feed.label.site_url": "URL Situs",
     "form.feed.label.feed_url": "URL Umpan",
+    "form.feed.label.description": "Deskripsi",
     "form.feed.label.category": "Kategori",
     "form.feed.label.crawler": "Ambil konten asli",
     "form.feed.label.feed_username": "Nama Pengguna Umpan",

+ 1 - 0
internal/locale/translations/it_IT.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Titolo",
     "form.feed.label.site_url": "URL del sito",
     "form.feed.label.feed_url": "URL del feed",
+    "form.feed.label.description": "Descrizione",
     "form.feed.label.category": "Categoria",
     "form.feed.label.crawler": "Scarica il contenuto integrale",
     "form.feed.label.feed_username": "Nome utente del feed",

+ 1 - 0
internal/locale/translations/ja_JP.json

@@ -307,6 +307,7 @@
     "form.feed.label.title": "タイトル",
     "form.feed.label.site_url": "サイト URL",
     "form.feed.label.feed_url": "フィード URL",
+    "form.feed.label.description": "説明",
     "form.feed.label.category": "カテゴリ",
     "form.feed.label.crawler": "オリジナルの内容を取得",
     "form.feed.label.feed_username": "フィードのユーザー名",

+ 1 - 0
internal/locale/translations/nl_NL.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Naam",
     "form.feed.label.site_url": "Website URL",
     "form.feed.label.feed_url": "Feed URL",
+    "form.feed.label.description": "Beschrijving",
     "form.feed.label.category": "Categorie",
     "form.feed.label.crawler": "Download originele content",
     "form.feed.label.feed_username": "Feed-gebruikersnaam",

+ 1 - 0
internal/locale/translations/pl_PL.json

@@ -327,6 +327,7 @@
     "form.feed.label.title": "Tytuł",
     "form.feed.label.site_url": "URL strony",
     "form.feed.label.feed_url": "URL kanału",
+    "form.feed.label.description": "Opis",
     "form.feed.label.category": "Kategoria",
     "form.feed.label.crawler": "Pobierz oryginalną treść",
     "form.feed.label.feed_username": "Subskrypcję nazwa użytkownika",

+ 1 - 0
internal/locale/translations/pt_BR.json

@@ -317,6 +317,7 @@
     "form.feed.label.title": "Título",
     "form.feed.label.site_url": "URL do site",
     "form.feed.label.feed_url": "URL da fonte",
+    "form.feed.label.description": "Descrição",
     "form.feed.label.category": "Categoria",
     "form.feed.label.crawler": "Obter conteúdo original",
     "form.feed.label.feed_username": "Nome de usuário da fonte",

+ 1 - 0
internal/locale/translations/ru_RU.json

@@ -327,6 +327,7 @@
     "form.feed.label.title": "Название",
     "form.feed.label.site_url": "Адрес сайта",
     "form.feed.label.feed_url": "Адрес подписки",
+    "form.feed.label.description": "Описание",
     "form.feed.label.category": "Категория",
     "form.feed.label.crawler": "Извлечь оригинальное содержимое",
     "form.feed.label.feed_username": "Имя пользователя подписки",

+ 1 - 0
internal/locale/translations/tr_TR.json

@@ -154,6 +154,7 @@
   "form.feed.label.disabled": "Bu beslemeyi yenileme",
   "form.feed.label.feed_password": "Besleme Parolası",
   "form.feed.label.feed_url": "Besleme URL'si",
+  "form.feed.label.description": "Açıklama",
   "form.feed.label.feed_username": "Besleme Kullanıcı Adı",
   "form.feed.label.fetch_via_proxy": "Proxy ile çek",
   "form.feed.label.hide_globally": "Genel okunmamış listesindeki girişleri gizle",

+ 1 - 0
internal/locale/translations/uk_UA.json

@@ -327,6 +327,7 @@
     "form.feed.label.title": "Назва",
     "form.feed.label.site_url": "URL-адреса сайту",
     "form.feed.label.feed_url": "URL-адреса стрічки",
+    "form.feed.label.description": "Опис",
     "form.feed.label.category": "Категорія",
     "form.feed.label.crawler": "Завантажувати оригінальний вміст",
     "form.feed.label.feed_username": "Ім’я користувача для завантаження",

+ 1 - 0
internal/locale/translations/zh_CN.json

@@ -307,6 +307,7 @@
     "form.feed.label.title": "标题",
     "form.feed.label.site_url": "源网站 URL",
     "form.feed.label.feed_url": "订阅源 URL",
+    "form.feed.label.description": "描述",
     "form.feed.label.category": "类别",
     "form.feed.label.crawler": "抓取全文内容",
     "form.feed.label.feed_username": "源用户名",

+ 1 - 0
internal/locale/translations/zh_TW.json

@@ -307,6 +307,7 @@
     "form.feed.label.title": "標題",
     "form.feed.label.site_url": "網站 URL",
     "form.feed.label.feed_url": "訂閱 Feed URL",
+    "form.feed.label.description": "描述",
     "form.feed.label.category": "類別",
     "form.feed.label.crawler": "下載原文內容",
     "form.feed.label.feed_username": "Feed 使用者名稱",

+ 6 - 0
internal/model/feed.go

@@ -28,6 +28,7 @@ type Feed struct {
 	FeedURL                     string    `json:"feed_url"`
 	SiteURL                     string    `json:"site_url"`
 	Title                       string    `json:"title"`
+	Description                 string    `json:"description"`
 	CheckedAt                   time.Time `json:"checked_at"`
 	NextCheckAt                 time.Time `json:"next_check_at"`
 	EtagHeader                  string    `json:"etag_header"`
@@ -167,6 +168,7 @@ type FeedModificationRequest struct {
 	FeedURL                     *string `json:"feed_url"`
 	SiteURL                     *string `json:"site_url"`
 	Title                       *string `json:"title"`
+	Description                 *string `json:"description"`
 	ScraperRules                *string `json:"scraper_rules"`
 	RewriteRules                *string `json:"rewrite_rules"`
 	BlocklistRules              *string `json:"blocklist_rules"`
@@ -201,6 +203,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
 		feed.Title = *f.Title
 	}
 
+	if f.Description != nil && *f.Description != "" {
+		feed.Description = *f.Description
+	}
+
 	if f.ScraperRules != nil {
 		feed.ScraperRules = *f.ScraperRules
 	}

+ 7 - 5
internal/reader/opml/handler.go

@@ -29,6 +29,7 @@ func (h *Handler) Export(userID int64) (string, error) {
 			Title:        feed.Title,
 			FeedURL:      feed.FeedURL,
 			SiteURL:      feed.SiteURL,
+			Description:  feed.Description,
 			CategoryName: feed.Category.Title,
 		})
 	}
@@ -68,11 +69,12 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
 			}
 
 			feed := &model.Feed{
-				UserID:   userID,
-				Title:    subscription.Title,
-				FeedURL:  subscription.FeedURL,
-				SiteURL:  subscription.SiteURL,
-				Category: category,
+				UserID:      userID,
+				Title:       subscription.Title,
+				FeedURL:     subscription.FeedURL,
+				SiteURL:     subscription.SiteURL,
+				Description: subscription.Description,
+				Category:    category,
 			}
 
 			h.store.CreateFeed(feed)

+ 6 - 5
internal/reader/opml/opml.go

@@ -27,11 +27,12 @@ type opmlHeader struct {
 }
 
 type opmlOutline struct {
-	Title    string                `xml:"title,attr,omitempty"`
-	Text     string                `xml:"text,attr"`
-	FeedURL  string                `xml:"xmlUrl,attr,omitempty"`
-	SiteURL  string                `xml:"htmlUrl,attr,omitempty"`
-	Outlines opmlOutlineCollection `xml:"outline,omitempty"`
+	Title       string                `xml:"title,attr,omitempty"`
+	Text        string                `xml:"text,attr"`
+	FeedURL     string                `xml:"xmlUrl,attr,omitempty"`
+	SiteURL     string                `xml:"htmlUrl,attr,omitempty"`
+	Description string                `xml:"description,attr,omitempty"`
+	Outlines    opmlOutlineCollection `xml:"outline,omitempty"`
 }
 
 func (outline opmlOutline) MarshalXML(e *xml.Encoder, start xml.StartElement) error {

+ 1 - 0
internal/reader/opml/parser.go

@@ -34,6 +34,7 @@ func getSubscriptionsFromOutlines(outlines opmlOutlineCollection, category strin
 				Title:        outline.GetTitle(),
 				FeedURL:      outline.FeedURL,
 				SiteURL:      outline.GetSiteURL(),
+				Description:  outline.Description,
 				CategoryName: category,
 			})
 		} else if outline.Outlines.HasChildren() {

+ 1 - 1
internal/reader/opml/parser_test.go

@@ -33,7 +33,7 @@ func TestParseOpmlWithoutCategories(t *testing.T) {
 	`
 
 	var expected SubcriptionList
-	expected = append(expected, &Subcription{Title: "CNET News.com", FeedURL: "http://news.com.com/2547-1_3-0-5.xml", SiteURL: "http://news.com.com/"})
+	expected = append(expected, &Subcription{Title: "CNET News.com", FeedURL: "http://news.com.com/2547-1_3-0-5.xml", SiteURL: "http://news.com.com/", Description: "Tech news and business reports by CNET News.com. Focused on information technology, core topics include computers, hardware, software, networking, and Internet media."})
 
 	subscriptions, err := Parse(bytes.NewBufferString(data))
 	if err != nil {

+ 5 - 4
internal/reader/opml/serializer.go

@@ -48,10 +48,11 @@ func convertSubscriptionsToOPML(subscriptions SubcriptionList) *opmlDocument {
 		category := opmlOutline{Text: categoryName, Outlines: make(opmlOutlineCollection, 0, len(groupedSubs[categoryName]))}
 		for _, subscription := range groupedSubs[categoryName] {
 			category.Outlines = append(category.Outlines, opmlOutline{
-				Title:   subscription.Title,
-				Text:    subscription.Title,
-				FeedURL: subscription.FeedURL,
-				SiteURL: subscription.SiteURL,
+				Title:       subscription.Title,
+				Text:        subscription.Title,
+				FeedURL:     subscription.FeedURL,
+				SiteURL:     subscription.SiteURL,
+				Description: subscription.Description,
 			})
 		}
 

+ 3 - 1
internal/reader/opml/subscription.go

@@ -9,12 +9,14 @@ type Subcription struct {
 	SiteURL      string
 	FeedURL      string
 	CategoryName string
+	Description  string
 }
 
 // Equals compare two subscriptions.
 func (s Subcription) Equals(subscription *Subcription) bool {
 	return s.Title == subscription.Title && s.SiteURL == subscription.SiteURL &&
-		s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName
+		s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName &&
+		s.Description == subscription.Description
 }
 
 // SubcriptionList is a list of subscriptions.

+ 2 - 0
internal/storage/entry_query_builder.go

@@ -281,6 +281,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
 			f.title as feed_title,
 			f.feed_url,
 			f.site_url,
+			f.description,
 			f.checked_at,
 			f.category_id,
 			c.title as category_title,
@@ -347,6 +348,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
 			&entry.Feed.Title,
 			&entry.Feed.FeedURL,
 			&entry.Feed.SiteURL,
+			&entry.Feed.Description,
 			&entry.Feed.CheckedAt,
 			&entry.Feed.Category.ID,
 			&entry.Feed.Category.Title,

+ 8 - 4
internal/storage/feed.go

@@ -238,10 +238,11 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
 			url_rewrite_rules,
 			no_media_player,
 			apprise_service_urls,
-			disable_http2
+			disable_http2,
+			description
 		)
 		VALUES
-			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
+			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)
 		RETURNING
 			id
 	`
@@ -272,6 +273,7 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
 		feed.NoMediaPlayer,
 		feed.AppriseServiceURLs,
 		feed.DisableHTTP2,
+		feed.Description,
 	).Scan(&feed.ID)
 	if err != nil {
 		return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
@@ -344,9 +346,10 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 			url_rewrite_rules=$25,
 			no_media_player=$26,
 			apprise_service_urls=$27,
-			disable_http2=$28
+			disable_http2=$28,
+			description=$29
 		WHERE
-			id=$29 AND user_id=$30
+			id=$30 AND user_id=$31
 	`
 	_, err = s.db.Exec(query,
 		feed.FeedURL,
@@ -377,6 +380,7 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 		feed.NoMediaPlayer,
 		feed.AppriseServiceURLs,
 		feed.DisableHTTP2,
+		feed.Description,
 		feed.ID,
 		feed.UserID,
 	)

+ 2 - 0
internal/storage/feed_query_builder.go

@@ -135,6 +135,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			f.feed_url,
 			f.site_url,
 			f.title,
+			f.description,
 			f.etag_header,
 			f.last_modified_header,
 			f.user_id,
@@ -202,6 +203,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			&feed.FeedURL,
 			&feed.SiteURL,
 			&feed.Title,
+			&feed.Description,
 			&feed.EtagHeader,
 			&feed.LastModifiedHeader,
 			&feed.UserID,

+ 3 - 0
internal/template/templates/views/edit_feed.html

@@ -63,6 +63,9 @@
             <label for="form-feed-url">{{ t "form.feed.label.feed_url" }}</label>
             <input type="url" name="feed_url" id="form-feed-url" placeholder="https://domain.tld/" value="{{ .form.FeedURL }}" spellcheck="false" required>
 
+            <label for="form-description">{{ t "form.feed.label.description" }}</label>
+            <textarea name="description" id="form-description" cols="40" rows="10" >{{ .form.Description }}</textarea>
+
             {{ if not .form.CategoryHidden }}
             <label><input type="checkbox" name="hide_globally" value="1"{{ if .form.HideGlobally }} checked{{ end }}> {{ t "form.feed.label.hide_globally" }}</label>
             {{ end }}

+ 1 - 0
internal/ui/feed_edit.go

@@ -43,6 +43,7 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
 		SiteURL:                     feed.SiteURL,
 		FeedURL:                     feed.FeedURL,
 		Title:                       feed.Title,
+		Description:                 feed.Description,
 		ScraperRules:                feed.ScraperRules,
 		RewriteRules:                feed.RewriteRules,
 		BlocklistRules:              feed.BlocklistRules,

+ 1 - 0
internal/ui/feed_update.go

@@ -59,6 +59,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
 		FeedURL:         model.OptionalString(feedForm.FeedURL),
 		SiteURL:         model.OptionalString(feedForm.SiteURL),
 		Title:           model.OptionalString(feedForm.Title),
+		Description:     model.OptionalString(feedForm.Description),
 		CategoryID:      model.OptionalNumber(feedForm.CategoryID),
 		BlocklistRules:  model.OptionalString(feedForm.BlocklistRules),
 		KeeplistRules:   model.OptionalString(feedForm.KeeplistRules),

+ 3 - 0
internal/ui/form/feed.go

@@ -15,6 +15,7 @@ type FeedForm struct {
 	FeedURL                     string
 	SiteURL                     string
 	Title                       string
+	Description                 string
 	ScraperRules                string
 	RewriteRules                string
 	BlocklistRules              string
@@ -43,6 +44,7 @@ func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
 	feed.Title = f.Title
 	feed.SiteURL = f.SiteURL
 	feed.FeedURL = f.FeedURL
+	feed.Description = f.Description
 	feed.ScraperRules = f.ScraperRules
 	feed.RewriteRules = f.RewriteRules
 	feed.BlocklistRules = f.BlocklistRules
@@ -76,6 +78,7 @@ func NewFeedForm(r *http.Request) *FeedForm {
 		FeedURL:                     r.FormValue("feed_url"),
 		SiteURL:                     r.FormValue("site_url"),
 		Title:                       r.FormValue("title"),
+		Description:                 r.FormValue("description"),
 		ScraperRules:                r.FormValue("scraper_rules"),
 		UserAgent:                   r.FormValue("user_agent"),
 		Cookie:                      r.FormValue("cookie"),