mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-24 21:19:19 +00:00
opml package
This commit is contained in:
parent
0d49377879
commit
1e65da9aa4
83
src/opml/builder.go
Normal file
83
src/opml/builder.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OPMLBuilder struct {
|
||||||
|
rootfolder *OPMLFolder
|
||||||
|
folders []*OPMLFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
type OPMLFolder struct {
|
||||||
|
title string
|
||||||
|
feeds []*OPMLFeed
|
||||||
|
}
|
||||||
|
|
||||||
|
type OPMLFeed struct {
|
||||||
|
title, description, feedUrl, siteUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuilder() *OPMLBuilder {
|
||||||
|
return &OPMLBuilder{
|
||||||
|
rootfolder: &OPMLFolder{feeds: make([]*OPMLFeed, 0)},
|
||||||
|
folders: make([]*OPMLFolder, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OPMLBuilder) AddFeed(title, description, feedUrl, siteUrl string) {
|
||||||
|
b.rootfolder.AddFeed(title, description, feedUrl, siteUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OPMLBuilder) AddFolder(title string) *OPMLFolder {
|
||||||
|
folder := &OPMLFolder{title: title}
|
||||||
|
b.folders = append(b.folders, folder)
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *OPMLFolder) AddFeed(title, description, feedUrl, siteUrl string) {
|
||||||
|
f.feeds = append(f.feeds, &OPMLFeed{title, description, feedUrl, siteUrl})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OPMLBuilder) String() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
|
||||||
|
line := func(s string, args ...string) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
escapedargs := make([]interface{}, len(args))
|
||||||
|
for idx, arg := range args {
|
||||||
|
escapedargs[idx] = html.EscapeString(arg)
|
||||||
|
}
|
||||||
|
s = fmt.Sprintf(s, escapedargs...)
|
||||||
|
}
|
||||||
|
builder.WriteString(s)
|
||||||
|
builder.WriteRune('\n')
|
||||||
|
}
|
||||||
|
feedline := func(feed *OPMLFeed, indent int) {
|
||||||
|
line(
|
||||||
|
strings.Repeat(" ", indent) + `<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`,
|
||||||
|
feed.title, feed.description,
|
||||||
|
feed.feedUrl, feed.siteUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
line(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||||
|
line(`<opml version="1.1">`)
|
||||||
|
line(`<head><title>Subscriptions</title></head>`)
|
||||||
|
line(`<body>`)
|
||||||
|
for _, folder := range b.folders {
|
||||||
|
line(` <outline text="%s">`, folder.title)
|
||||||
|
for _, feed := range folder.feeds {
|
||||||
|
feedline(feed, 4)
|
||||||
|
}
|
||||||
|
line(` </outline>`)
|
||||||
|
}
|
||||||
|
for _, feed := range b.rootfolder.feeds {
|
||||||
|
feedline(feed, 2)
|
||||||
|
}
|
||||||
|
line(`</body>`)
|
||||||
|
line(`</opml>`)
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
30
src/opml/builder_test.go
Normal file
30
src/opml/builder_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package opml
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var sample = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<opml version="1.1">
|
||||||
|
<head><title>Subscriptions</title></head>
|
||||||
|
<body>
|
||||||
|
<outline text="sub">
|
||||||
|
<outline type="rss" text="subtitle1" description="sub1" xmlUrl="https://foo.com/feed.xml" htmlUrl="https://foo.com/"/>
|
||||||
|
<outline type="rss" text="&>" description="<>" xmlUrl="https://bar.com/feed.xml" htmlUrl="https://bar.com/"/>
|
||||||
|
</outline>
|
||||||
|
<outline type="rss" text="title1" description="desc1" xmlUrl="https://example.com/feed.xml" htmlUrl="https://example.com/"/>
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestOPMLBuilder(t *testing.T) {
|
||||||
|
builder := NewBuilder()
|
||||||
|
builder.AddFeed("title1", "desc1", "https://example.com/feed.xml", "https://example.com/")
|
||||||
|
|
||||||
|
folder := builder.AddFolder("sub")
|
||||||
|
folder.AddFeed("subtitle1", "sub1", "https://foo.com/feed.xml", "https://foo.com/")
|
||||||
|
folder.AddFeed("&>", "<>", "https://bar.com/feed.xml", "https://bar.com/")
|
||||||
|
|
||||||
|
output := builder.String()
|
||||||
|
if output != sample {
|
||||||
|
t.Errorf("\n=== expected:\n%s\n=== got:\n%s\n===", sample, output)
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,27 @@
|
|||||||
package server
|
package opml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type opml struct {
|
type OPML struct {
|
||||||
XMLName xml.Name `xml:"opml"`
|
XMLName xml.Name `xml:"opml"`
|
||||||
Version string `xml:"version,attr"`
|
Version string `xml:"version,attr"`
|
||||||
Outlines []outline `xml:"body>outline"`
|
Outlines []Outline `xml:"body>outline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type outline struct {
|
type Outline struct {
|
||||||
Type string `xml:"type,attr,omitempty"`
|
Type string `xml:"type,attr,omitempty"`
|
||||||
Title string `xml:"text,attr"`
|
Title string `xml:"text,attr"`
|
||||||
FeedURL string `xml:"xmlUrl,attr,omitempty"`
|
FeedURL string `xml:"xmlUrl,attr,omitempty"`
|
||||||
SiteURL string `xml:"htmlUrl,attr,omitempty"`
|
SiteURL string `xml:"htmlUrl,attr,omitempty"`
|
||||||
Description string `xml:"description,attr,omitempty"`
|
Description string `xml:"description,attr,omitempty"`
|
||||||
Outlines []outline `xml:"outline,omitempty"`
|
Outlines []Outline `xml:"outline,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o outline) AllFeeds() []outline {
|
func (o Outline) AllFeeds() []Outline {
|
||||||
result := make([]outline, 0)
|
result := make([]Outline, 0)
|
||||||
for _, sub := range o.Outlines {
|
for _, sub := range o.Outlines {
|
||||||
if sub.Type == "rss" {
|
if sub.Type == "rss" {
|
||||||
result = append(result, sub)
|
result = append(result, sub)
|
||||||
@ -32,8 +32,8 @@ func (o outline) AllFeeds() []outline {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOPML(r io.Reader) (*opml, error) {
|
func Parse(r io.Reader) (*OPML, error) {
|
||||||
feeds := new(opml)
|
feeds := new(OPML)
|
||||||
decoder := xml.NewDecoder(r)
|
decoder := xml.NewDecoder(r)
|
||||||
decoder.Entity = xml.HTMLEntity
|
decoder.Entity = xml.HTMLEntity
|
||||||
decoder.Strict = false
|
decoder.Strict = false
|
@ -2,19 +2,17 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"github.com/nkanaev/yarr/src/assets"
|
"github.com/nkanaev/yarr/src/assets"
|
||||||
"github.com/nkanaev/yarr/src/auth"
|
"github.com/nkanaev/yarr/src/auth"
|
||||||
"github.com/nkanaev/yarr/src/router"
|
"github.com/nkanaev/yarr/src/router"
|
||||||
"github.com/nkanaev/yarr/src/storage"
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
"html"
|
"github.com/nkanaev/yarr/src/opml"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handler() http.Handler {
|
func (s *Server) handler() http.Handler {
|
||||||
@ -338,7 +336,7 @@ func (s *Server) handleOPMLImport(c *router.Context) {
|
|||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doc, err := parseOPML(file)
|
doc, err := opml.Parse(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
@ -365,57 +363,36 @@ func (s *Server) handleOPMLExport(c *router.Context) {
|
|||||||
c.Out.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
c.Out.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
c.Out.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`)
|
c.Out.Header().Set("Content-Disposition", `attachment; filename="subscriptions.opml"`)
|
||||||
|
|
||||||
builder := strings.Builder{}
|
rootFeeds := make([]*storage.Feed, 0)
|
||||||
|
feedsByFolderID := make(map[int64][]*storage.Feed)
|
||||||
line := func(s string, args ...string) {
|
|
||||||
if len(args) > 0 {
|
|
||||||
escapedargs := make([]interface{}, len(args))
|
|
||||||
for idx, arg := range args {
|
|
||||||
escapedargs[idx] = html.EscapeString(arg)
|
|
||||||
}
|
|
||||||
s = fmt.Sprintf(s, escapedargs...)
|
|
||||||
}
|
|
||||||
builder.WriteString(s)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
feedline := func(feed storage.Feed, indent int) {
|
|
||||||
line(
|
|
||||||
strings.Repeat(" ", indent)+
|
|
||||||
`<outline type="rss" text="%s" description="%s" xmlUrl="%s" htmlUrl="%s"/>`,
|
|
||||||
feed.Title, feed.Description,
|
|
||||||
feed.FeedLink, feed.Link,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
line(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
||||||
line(`<opml version="1.1">`)
|
|
||||||
line(`<head>`)
|
|
||||||
line(` <title>subscriptions.opml</title>`)
|
|
||||||
line(`</head>`)
|
|
||||||
line(`<body>`)
|
|
||||||
feedsByFolderID := make(map[int64][]storage.Feed)
|
|
||||||
for _, feed := range s.db.ListFeeds() {
|
for _, feed := range s.db.ListFeeds() {
|
||||||
var folderId = int64(0)
|
feed := feed
|
||||||
if feed.FolderId != nil {
|
if feed.FolderId == nil {
|
||||||
folderId = *feed.FolderId
|
rootFeeds = append(rootFeeds, &feed)
|
||||||
|
} else {
|
||||||
|
id := *feed.FolderId
|
||||||
|
if feedsByFolderID[id] == nil {
|
||||||
|
feedsByFolderID[id] = make([]*storage.Feed, 0)
|
||||||
|
}
|
||||||
|
feedsByFolderID[id] = append(feedsByFolderID[id], &feed)
|
||||||
}
|
}
|
||||||
if feedsByFolderID[folderId] == nil {
|
}
|
||||||
feedsByFolderID[folderId] = make([]storage.Feed, 0)
|
builder := opml.NewBuilder()
|
||||||
}
|
|
||||||
feedsByFolderID[folderId] = append(feedsByFolderID[folderId], feed)
|
for _, feed := range rootFeeds {
|
||||||
|
builder.AddFeed(feed.Title, feed.Description, feed.FeedLink, feed.Link)
|
||||||
}
|
}
|
||||||
for _, folder := range s.db.ListFolders() {
|
for _, folder := range s.db.ListFolders() {
|
||||||
line(` <outline text="%s">`, folder.Title)
|
folderFeeds := feedsByFolderID[folder.Id]
|
||||||
for _, feed := range feedsByFolderID[folder.Id] {
|
if len(folderFeeds) == 0 {
|
||||||
feedline(feed, 4)
|
continue
|
||||||
|
}
|
||||||
|
feedFolder := builder.AddFolder(folder.Title)
|
||||||
|
for _, feed := range folderFeeds {
|
||||||
|
feedFolder.AddFeed(feed.Title, feed.Description, feed.FeedLink, feed.Link)
|
||||||
}
|
}
|
||||||
line(` </outline>`)
|
|
||||||
}
|
}
|
||||||
for _, feed := range feedsByFolderID[0] {
|
|
||||||
feedline(feed, 2)
|
|
||||||
}
|
|
||||||
line(`</body>`)
|
|
||||||
line(`</opml>`)
|
|
||||||
c.Out.Write([]byte(builder.String()))
|
c.Out.Write([]byte(builder.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user