mirror of
				https://github.com/nkanaev/yarr.git
				synced 2025-10-30 06:31:07 +00:00 
			
		
		
		
	opml package
This commit is contained in:
		
							
								
								
									
										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())) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user