mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Refactor builders
This commit is contained in:
+6
-4
@@ -47,15 +47,17 @@ func main() {
|
||||
}
|
||||
|
||||
// Create web server
|
||||
if cfg.Port == 0 {
|
||||
log.Debug("using default port 8080")
|
||||
cfg.Port = 8080
|
||||
port := cfg.Server.Port
|
||||
if port == 0 {
|
||||
port = 8080
|
||||
}
|
||||
|
||||
srv := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
}
|
||||
|
||||
log.Debugf("using address %s", srv.Addr)
|
||||
|
||||
// Run listener
|
||||
go func() {
|
||||
log.Infof("running listener at %s", srv.Addr)
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
module github.com/mxpv/podsync
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.25.0 // indirect
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7
|
||||
github.com/aws/aws-sdk-go v1.15.81
|
||||
github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b // indirect
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851 // indirect
|
||||
github.com/eduncan911/podcast v1.3.0 // indirect
|
||||
github.com/garyburd/redigo v1.6.0 // indirect
|
||||
github.com/gin-contrib/cache v1.1.0
|
||||
github.com/gin-contrib/gzip v0.0.1
|
||||
github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61
|
||||
github.com/gin-gonic/gin v1.3.0
|
||||
github.com/go-pg/pg v6.14.2+incompatible
|
||||
github.com/golang/mock v1.2.0
|
||||
github.com/gorilla/sessions v1.1.1 // indirect
|
||||
github.com/golang/protobuf v1.2.0 // indirect
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
|
||||
github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f // indirect
|
||||
github.com/mxpv/patreon-go v0.0.0-20180807002359-67dbab1ad14c
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18
|
||||
github.com/onsi/ginkgo v1.7.0 // indirect
|
||||
github.com/onsi/gomega v1.4.3 // indirect
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c
|
||||
github.com/sirupsen/logrus v1.2.0
|
||||
@@ -33,10 +17,11 @@ require (
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
||||
golang.org/x/sys v0.0.0-20190311152110-c8c8c57fd1e1 // indirect
|
||||
google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56
|
||||
google.golang.org/appengine v1.1.0 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
||||
@@ -1,88 +1,25 @@
|
||||
cloud.google.com/go v0.25.0 h1:6vD6xZTc8Jo6To8gHxFDRVsMvWFDgY3rugNszcDalN8=
|
||||
cloud.google.com/go v0.25.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec h1:1VPruZMM1WQC7POhjxbZOWK564cuFz1hlpwYW6ocM4E=
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec/go.mod h1:+hWo/MWgY8VtjZvdrYM2nPRMaK40zX2iPsH/qD0+Xs0=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 h1:Clo7QBZv+fHzjCgVp4ELlbIsY5rScCmj+4VCfoMfqtQ=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
||||
github.com/aws/aws-sdk-go v1.15.81 h1:va7uoFaV9uKAtZ6BTmp1u7paoMsizYRRLvRuoC07nQ8=
|
||||
github.com/aws/aws-sdk-go v1.15.81/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
|
||||
github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b h1:PfxLkkgJYE095CKZji++BNwZjxWfoAF21WFPzkzOZEs=
|
||||
github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b/go.mod h1:5r9chGCb4uUhBCGMDDCYfyHU/awSRoBeG53Zaj1crhU=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851 h1:kvC96NV3jN3viax6pJqsa8R4aQSapayHdDpNsBF/EN8=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eduncan911/podcast v1.3.0 h1:lVCar1J39mMNWR2SbGzPjeUbCKEkQ6/pt/7beQqK6fk=
|
||||
github.com/eduncan911/podcast v1.3.0/go.mod h1:C7Q04QZtv7LW/1X67mc1zwsktpZ68kbxsUS3CYWniJg=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/gin-contrib/cache v1.1.0 h1:lM8B4YtzdQQM6ThTlvtNPeBNfW1mNdh/CMFQfenH1dk=
|
||||
github.com/gin-contrib/cache v1.1.0/go.mod h1:9ylpYjLq309/y5hTpyuDxfPG+V6QlSB56vrWe6OhoLQ=
|
||||
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
|
||||
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
||||
github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61 h1:PK7FuRfHB1/teDEyN/+STsl3BHJn3Uf7ZbZTknsSj0Q=
|
||||
github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61/go.mod h1:F4TwIEOO0sgMULDVvcP85n9Jl8RPcOWfs17D0/fCWcE=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/go-pg/pg v6.14.2+incompatible h1:FrOgsHDUhC3V3wkBGAIN5LVj4nJczFPyy1YNFnetfIQ=
|
||||
github.com/go-pg/pg v6.14.2+incompatible/go.mod h1:a2oXow+aFOrvwcKs3eIA0lNFmMilrxK2sOkB5NWe0vA=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f h1:84d0qxD9AiuBNpeK5TkYwTKKNezsYxIVn8nWh0pq51E=
|
||||
github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mxpv/patreon-go v0.0.0-20180807002359-67dbab1ad14c h1:glRDvmNgmckYjQHMQ17XFlci6RehtmGmRQV+Cq6FusI=
|
||||
github.com/mxpv/patreon-go v0.0.0-20180807002359-67dbab1ad14c/go.mod h1:ksYjm2GAbGlgIP7jO9Q5/AdyE4MwwEbgQ+lFMx3hyiM=
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18 h1:YYsu49Y42JA+CSs9+z2MGBdGxb5jklpagLp5QPJ6BwQ=
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18/go.mod h1:bKrqwMF8O3PciTG92w0992h/d7Aj7CuIF5uTNEl3pNY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
||||
github.com/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c h1:KhHx/Ta3c9C1gcSo5UhDeo/D4JnhnxJTrlcOEOFiMfY=
|
||||
github.com/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c/go.mod h1:10FeaKUMy5t3KLsYfy54dFrq0rpwcfyKkKcF7vRGIRY=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
@@ -92,8 +29,6 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f h1:y3Vj7GoDdcBkxFa2RUUFKM25TrBbWVDnjRDI0u975zQ=
|
||||
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf h1:cgAKVljim9RJRcJNGjnBUajXj1FupBSdWwW4JaQG7vk=
|
||||
github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:6rZqAOk/eYX5FJyjQJ6Z3RBSN389IXX2ijwW4FcggaM=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
@@ -102,20 +37,13 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd h1:QQhib242ErYDSMitlBm8V7wYCm/1a25hV8qMadIKLPA=
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190311152110-c8c8c57fd1e1 h1:FQNj2xvjQ1lgFyzbSybGZr792Y8Dy95D7uuqnZAzNaA=
|
||||
golang.org/x/sys v0.0.0-20190311152110-c8c8c57fd1e1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -127,15 +55,3 @@ google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRS
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/link"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
// ID of episode
|
||||
ID string
|
||||
Title string
|
||||
Description string
|
||||
Thumbnail string
|
||||
Duration int64
|
||||
VideoURL string
|
||||
PubDate time.Time
|
||||
Size int64
|
||||
Order string
|
||||
}
|
||||
|
||||
type Feed struct {
|
||||
FeedID string
|
||||
ItemID string
|
||||
LinkType link.Type // Either group, channel or user
|
||||
Provider link.Provider // Youtube or Vimeo
|
||||
CreatedAt time.Time
|
||||
LastAccess time.Time
|
||||
ExpirationTime time.Time
|
||||
CoverArt string
|
||||
Explicit bool
|
||||
Language string // ISO 639
|
||||
Title string
|
||||
Description string
|
||||
PubDate time.Time
|
||||
Author string
|
||||
ItemURL string // Platform specific URL
|
||||
Episodes []*Item // Array of episodes, serialized as gziped EpisodesData in DynamoDB
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package builders
|
||||
package builder
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/link"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,19 +23,19 @@ type VimeoBuilder struct {
|
||||
client *vimeo.Client
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) selectImage(p *vimeo.Pictures, q api.Quality) string {
|
||||
func (v *VimeoBuilder) selectImage(p *vimeo.Pictures, q config.Quality) string {
|
||||
if p == nil || len(p.Sizes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if q == api.QualityLow {
|
||||
if q == config.QualityLow {
|
||||
return p.Sizes[0].Link
|
||||
}
|
||||
|
||||
return p.Sizes[len(p.Sizes)-1].Link
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) queryChannel(feed *model.Feed) error {
|
||||
func (v *VimeoBuilder) queryChannel(feed *Feed, cfg *config.Feed) error {
|
||||
channelID := feed.ItemID
|
||||
|
||||
ch, resp, err := v.client.Channels.Get(channelID)
|
||||
@@ -49,7 +50,7 @@ func (v *VimeoBuilder) queryChannel(feed *model.Feed) error {
|
||||
feed.Title = ch.Name
|
||||
feed.ItemURL = ch.Link
|
||||
feed.Description = ch.Description
|
||||
feed.CoverArt = v.selectImage(ch.Pictures, feed.Quality)
|
||||
feed.CoverArt = v.selectImage(ch.Pictures, cfg.Quality)
|
||||
feed.Author = ch.User.Name
|
||||
feed.PubDate = ch.CreatedTime
|
||||
feed.UpdatedAt = time.Now().UTC()
|
||||
@@ -57,7 +58,7 @@ func (v *VimeoBuilder) queryChannel(feed *model.Feed) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) queryGroup(feed *model.Feed) error {
|
||||
func (v *VimeoBuilder) queryGroup(feed *Feed, cfg *config.Feed) error {
|
||||
groupID := feed.ItemID
|
||||
|
||||
gr, resp, err := v.client.Groups.Get(groupID)
|
||||
@@ -72,7 +73,7 @@ func (v *VimeoBuilder) queryGroup(feed *model.Feed) error {
|
||||
feed.Title = gr.Name
|
||||
feed.ItemURL = gr.Link
|
||||
feed.Description = gr.Description
|
||||
feed.CoverArt = v.selectImage(gr.Pictures, feed.Quality)
|
||||
feed.CoverArt = v.selectImage(gr.Pictures, cfg.Quality)
|
||||
feed.Author = gr.User.Name
|
||||
feed.PubDate = gr.CreatedTime
|
||||
feed.UpdatedAt = time.Now().UTC()
|
||||
@@ -80,7 +81,7 @@ func (v *VimeoBuilder) queryGroup(feed *model.Feed) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) queryUser(feed *model.Feed) error {
|
||||
func (v *VimeoBuilder) queryUser(feed *Feed, cfg *config.Feed) error {
|
||||
userID := feed.ItemID
|
||||
|
||||
user, resp, err := v.client.Users.Get(userID)
|
||||
@@ -95,7 +96,7 @@ func (v *VimeoBuilder) queryUser(feed *model.Feed) error {
|
||||
feed.Title = user.Name
|
||||
feed.ItemURL = user.Link
|
||||
feed.Description = user.Bio
|
||||
feed.CoverArt = v.selectImage(user.Pictures, feed.Quality)
|
||||
feed.CoverArt = v.selectImage(user.Pictures, cfg.Quality)
|
||||
feed.Author = user.Name
|
||||
feed.PubDate = user.CreatedTime
|
||||
feed.UpdatedAt = time.Now().UTC()
|
||||
@@ -110,20 +111,12 @@ func (v *VimeoBuilder) getVideoSize(video *vimeo.Video) int64 {
|
||||
|
||||
type getVideosFunc func(string, ...vimeo.CallOption) ([]*vimeo.Video, *vimeo.Response, error)
|
||||
|
||||
func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *model.Feed) error {
|
||||
func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *Feed, cfg *config.Feed) error {
|
||||
var (
|
||||
page = 1
|
||||
added = 0
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if len(feed.Episodes) > 0 {
|
||||
feed.LastID = feed.Episodes[0].ID
|
||||
} else {
|
||||
feed.LastID = ""
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
videos, response, err := getVideos(feed.ItemID, vimeo.OptPage(page), vimeo.OptPerPage(vimeoDefaultPageSize))
|
||||
if err != nil {
|
||||
@@ -140,16 +133,16 @@ func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *model.Feed) er
|
||||
videoURL = video.Link
|
||||
duration = int64(video.Duration)
|
||||
size = v.getVideoSize(video)
|
||||
image = v.selectImage(video.Pictures, feed.Quality)
|
||||
image = v.selectImage(video.Pictures, cfg.Quality)
|
||||
)
|
||||
|
||||
feed.Episodes = append(feed.Episodes, &model.Item{
|
||||
feed.Episodes = append(feed.Episodes, &Item{
|
||||
ID: videoID,
|
||||
Title: video.Name,
|
||||
Description: video.Description,
|
||||
Duration: duration,
|
||||
Size: size,
|
||||
PubDate: model.Timestamp(video.CreatedTime),
|
||||
PubDate: video.CreatedTime,
|
||||
Thumbnail: image,
|
||||
VideoURL: videoURL,
|
||||
})
|
||||
@@ -157,7 +150,7 @@ func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *model.Feed) er
|
||||
added++
|
||||
}
|
||||
|
||||
if added >= feed.PageSize || response.NextPage == "" {
|
||||
if added >= cfg.PageSize || response.NextPage == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,50 +158,51 @@ func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *model.Feed) er
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) Build(feed *model.Feed) error {
|
||||
feed.Episodes = []*model.Item{}
|
||||
|
||||
if feed.LinkType == api.LinkTypeChannel {
|
||||
if err := v.queryChannel(feed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.queryVideos(v.client.Channels.ListVideo, feed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (v *VimeoBuilder) Build(cfg *config.Feed) (*Feed, error) {
|
||||
info, err := link.Parse(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feed.LinkType == api.LinkTypeGroup {
|
||||
if err := v.queryGroup(feed); err != nil {
|
||||
return err
|
||||
feed := &Feed{}
|
||||
|
||||
if info.LinkType == link.TypeChannel {
|
||||
if err := v.queryChannel(feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := v.queryVideos(v.client.Groups.ListVideo, feed); err != nil {
|
||||
return err
|
||||
if err := v.queryVideos(v.client.Channels.ListVideo, feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
if feed.LinkType == api.LinkTypeUser {
|
||||
if err := v.queryUser(feed); err != nil {
|
||||
return err
|
||||
if info.LinkType == link.TypeGroup {
|
||||
if err := v.queryGroup(feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := v.queryVideos(v.client.Users.ListVideo, feed); err != nil {
|
||||
return err
|
||||
if err := v.queryVideos(v.client.Groups.ListVideo, feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
return errors.New("unsupported feed type")
|
||||
}
|
||||
if info.LinkType == link.TypeUser {
|
||||
if err := v.queryUser(feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) GetVideoCount(feed *model.Feed) (uint64, error) {
|
||||
return 0, errors.New("not supported")
|
||||
if err := v.queryVideos(v.client.Users.ListVideo, feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported feed type")
|
||||
}
|
||||
|
||||
func NewVimeoBuilder(ctx context.Context, token string) (*VimeoBuilder, error) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package builders
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,8 +22,8 @@ func TestQueryVimeoChannel(t *testing.T) {
|
||||
builder, err := NewVimeoBuilder(context.Background(), vimeoKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
podcast := &model.Feed{ItemID: "staffpicks", Quality: api.QualityHigh}
|
||||
err = builder.queryChannel(podcast)
|
||||
podcast := &Feed{ItemID: "staffpicks"}
|
||||
err = builder.queryChannel(podcast, &config.Feed{Quality: config.QualityHigh})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "https://vimeo.com/channels/staffpicks", podcast.ItemURL)
|
||||
@@ -42,8 +41,8 @@ func TestQueryVimeoGroup(t *testing.T) {
|
||||
builder, err := NewVimeoBuilder(context.Background(), vimeoKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
podcast := &model.Feed{ItemID: "motion", Quality: api.QualityHigh}
|
||||
err = builder.queryGroup(podcast)
|
||||
podcast := &Feed{ItemID: "motion"}
|
||||
err = builder.queryGroup(podcast, &config.Feed{Quality: config.QualityHigh})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "https://vimeo.com/groups/motion", podcast.ItemURL)
|
||||
@@ -61,8 +60,8 @@ func TestQueryVimeoUser(t *testing.T) {
|
||||
builder, err := NewVimeoBuilder(context.Background(), vimeoKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
podcast := &model.Feed{ItemID: "motionarray", Quality: api.QualityHigh}
|
||||
err = builder.queryUser(podcast)
|
||||
podcast := &Feed{ItemID: "motionarray"}
|
||||
err = builder.queryUser(podcast, &config.Feed{Quality: config.QualityHigh})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "https://vimeo.com/motionarray", podcast.ItemURL)
|
||||
@@ -79,9 +78,9 @@ func TestQueryVimeoVideos(t *testing.T) {
|
||||
builder, err := NewVimeoBuilder(context.Background(), vimeoKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
feed := &model.Feed{ItemID: "staffpicks"}
|
||||
feed := &Feed{ItemID: "staffpicks"}
|
||||
|
||||
err = builder.queryVideos(builder.client.Channels.ListVideo, feed)
|
||||
err = builder.queryVideos(builder.client.Channels.ListVideo, feed, &config.Feed{Quality: config.QualityHigh})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, vimeoDefaultPageSize, len(feed.Episodes))
|
||||
@@ -1,4 +1,4 @@
|
||||
package builders
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"google.golang.org/api/youtube/v3"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/link"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,13 +38,13 @@ type YouTubeBuilder struct {
|
||||
|
||||
// Cost: 5 units (call method: 1, snippet: 2, contentDetails: 2)
|
||||
// See https://developers.google.com/youtube/v3/docs/channels/list#part
|
||||
func (yt *YouTubeBuilder) listChannels(linkType api.LinkType, id string, parts string) (*youtube.Channel, error) {
|
||||
func (yt *YouTubeBuilder) listChannels(linkType link.Type, id string, parts string) (*youtube.Channel, error) {
|
||||
req := yt.client.Channels.List(parts)
|
||||
|
||||
switch linkType {
|
||||
case api.LinkTypeChannel:
|
||||
case link.TypeChannel:
|
||||
req = req.Id(id)
|
||||
case api.LinkTypeUser:
|
||||
case link.TypeUser:
|
||||
req = req.ForUsername(id)
|
||||
default:
|
||||
return nil, errors.New("unsupported link type")
|
||||
@@ -111,7 +112,7 @@ func (yt *YouTubeBuilder) parseDate(s string) (time.Time, error) {
|
||||
return date, nil
|
||||
}
|
||||
|
||||
func (yt *YouTubeBuilder) selectThumbnail(snippet *youtube.ThumbnailDetails, quality api.Quality, videoID string) string {
|
||||
func (yt *YouTubeBuilder) selectThumbnail(snippet *youtube.ThumbnailDetails, quality config.Quality, videoID string) string {
|
||||
if snippet == nil {
|
||||
if videoID != "" {
|
||||
return fmt.Sprintf("https://img.youtube.com/vi/%s/default.jpg", videoID)
|
||||
@@ -123,7 +124,7 @@ func (yt *YouTubeBuilder) selectThumbnail(snippet *youtube.ThumbnailDetails, qua
|
||||
|
||||
// Use high resolution thumbnails for high quality mode
|
||||
// https://github.com/mxpv/Podsync/issues/14
|
||||
if quality == api.QualityHigh {
|
||||
if quality == config.QualityHigh {
|
||||
if snippet.Maxres != nil {
|
||||
return snippet.Maxres.Url
|
||||
}
|
||||
@@ -140,19 +141,19 @@ func (yt *YouTubeBuilder) selectThumbnail(snippet *youtube.ThumbnailDetails, qua
|
||||
return snippet.Default.Url
|
||||
}
|
||||
|
||||
func (yt *YouTubeBuilder) GetVideoCount(feed *model.Feed) (uint64, error) {
|
||||
switch feed.LinkType {
|
||||
case api.LinkTypeChannel, api.LinkTypeUser:
|
||||
func (yt *YouTubeBuilder) GetVideoCount(info *link.Info) (uint64, error) {
|
||||
switch info.LinkType {
|
||||
case link.TypeChannel, link.TypeUser:
|
||||
// Cost: 3 units
|
||||
if channel, err := yt.listChannels(feed.LinkType, feed.ItemID, "id,statistics"); err != nil {
|
||||
if channel, err := yt.listChannels(info.LinkType, info.ItemID, "id,statistics"); err != nil {
|
||||
return 0, err
|
||||
} else { // nolint:golint
|
||||
return channel.Statistics.VideoCount, nil
|
||||
}
|
||||
|
||||
case api.LinkTypePlaylist:
|
||||
case link.TypePlaylist:
|
||||
// Cost: 3 units
|
||||
if playlist, err := yt.listPlaylists(feed.ItemID, "", "id,contentDetails"); err != nil {
|
||||
if playlist, err := yt.listPlaylists(info.ItemID, "", "id,contentDetails"); err != nil {
|
||||
return 0, err
|
||||
} else { // nolint:golint
|
||||
return uint64(playlist.ContentDetails.ItemCount), nil
|
||||
@@ -163,104 +164,94 @@ func (yt *YouTubeBuilder) GetVideoCount(feed *model.Feed) (uint64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (yt *YouTubeBuilder) queryFeed(feed *model.Feed) (string, error) {
|
||||
func (yt *YouTubeBuilder) queryFeed(info *link.Info, config *config.Feed) (*Feed, error) {
|
||||
var (
|
||||
title string
|
||||
desc string
|
||||
link string // URL link to YouTube resource
|
||||
itemID string // ID of YouTube's channel, user or playlist
|
||||
author string
|
||||
pubDate time.Time
|
||||
thumbnails *youtube.ThumbnailDetails
|
||||
)
|
||||
|
||||
switch feed.LinkType {
|
||||
case api.LinkTypeChannel, api.LinkTypeUser:
|
||||
feed := Feed{
|
||||
ItemID: info.ItemID,
|
||||
Provider: info.Provider,
|
||||
LinkType: info.LinkType,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
switch info.LinkType {
|
||||
case link.TypeChannel, link.TypeUser:
|
||||
// Cost: 5 units for channel or user
|
||||
channel, err := yt.listChannels(feed.LinkType, feed.ItemID, "id,snippet,contentDetails")
|
||||
channel, err := yt.listChannels(info.LinkType, info.ItemID, "id,snippet,contentDetails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
title = channel.Snippet.Title
|
||||
desc = channel.Snippet.Description
|
||||
feed.Title = channel.Snippet.Title
|
||||
feed.Description = channel.Snippet.Description
|
||||
|
||||
if channel.Kind == "youtube#channel" {
|
||||
link = fmt.Sprintf("https://youtube.com/channel/%s", channel.Id)
|
||||
author = title
|
||||
feed.ItemURL = fmt.Sprintf("https://youtube.com/channel/%s", channel.Id)
|
||||
feed.Author = "<notfound>"
|
||||
} else {
|
||||
link = fmt.Sprintf("https://youtube.com/user/%s", channel.Snippet.CustomUrl)
|
||||
author = channel.Snippet.CustomUrl
|
||||
feed.ItemURL = fmt.Sprintf("https://youtube.com/user/%s", channel.Snippet.CustomUrl)
|
||||
feed.Author = channel.Snippet.CustomUrl
|
||||
}
|
||||
|
||||
itemID = channel.ContentDetails.RelatedPlaylists.Uploads
|
||||
feed.ItemID = channel.ContentDetails.RelatedPlaylists.Uploads
|
||||
|
||||
if date, err := yt.parseDate(channel.Snippet.PublishedAt); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
} else { // nolint:golint
|
||||
pubDate = date
|
||||
feed.PubDate = date
|
||||
}
|
||||
|
||||
thumbnails = channel.Snippet.Thumbnails
|
||||
|
||||
case api.LinkTypePlaylist:
|
||||
case link.TypePlaylist:
|
||||
// Cost: 3 units for playlist
|
||||
playlist, err := yt.listPlaylists(feed.ItemID, "", "id,snippet")
|
||||
playlist, err := yt.listPlaylists(info.ItemID, "", "id,snippet")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("%s: %s", playlist.Snippet.ChannelTitle, playlist.Snippet.Title)
|
||||
desc = playlist.Snippet.Description
|
||||
feed.Title = fmt.Sprintf("%s: %s", playlist.Snippet.ChannelTitle, playlist.Snippet.Title)
|
||||
feed.Description = playlist.Snippet.Description
|
||||
|
||||
link = fmt.Sprintf("https://youtube.com/playlist?list=%s", playlist.Id)
|
||||
feed.ItemURL = fmt.Sprintf("https://youtube.com/playlist?list=%s", playlist.Id)
|
||||
feed.ItemID = playlist.Id
|
||||
|
||||
itemID = playlist.Id
|
||||
|
||||
author = title
|
||||
feed.Author = "<notfound>"
|
||||
|
||||
if date, err := yt.parseDate(playlist.Snippet.PublishedAt); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
} else { // nolint:golint
|
||||
pubDate = date
|
||||
feed.PubDate = date
|
||||
}
|
||||
|
||||
thumbnails = playlist.Snippet.Thumbnails
|
||||
|
||||
default:
|
||||
return "", errors.New("unsupported link format")
|
||||
return nil, errors.New("unsupported link format")
|
||||
}
|
||||
|
||||
// Apply customizations and default values
|
||||
|
||||
if desc == "" {
|
||||
desc = fmt.Sprintf("%s (%s)", title, pubDate)
|
||||
if feed.Description == "" {
|
||||
feed.Description = fmt.Sprintf("%s (%s)", feed.Title, feed.PubDate)
|
||||
}
|
||||
|
||||
var image string
|
||||
if feed.CoverArt != "" {
|
||||
image = feed.CoverArt
|
||||
if config.CoverArt != "" {
|
||||
feed.CoverArt = config.CoverArt
|
||||
} else {
|
||||
image = yt.selectThumbnail(thumbnails, feed.Quality, "")
|
||||
feed.CoverArt = yt.selectThumbnail(thumbnails, config.Quality, "")
|
||||
}
|
||||
|
||||
// New interface
|
||||
feed.Title = title
|
||||
feed.Description = desc
|
||||
feed.Author = author
|
||||
feed.ItemURL = link
|
||||
feed.UpdatedAt = time.Now().UTC()
|
||||
feed.PubDate = pubDate
|
||||
feed.CoverArt = image
|
||||
|
||||
return itemID, nil
|
||||
return &feed, nil
|
||||
}
|
||||
|
||||
// Video size information requires 1 additional call for each video (1 feed = 50 videos = 50 calls),
|
||||
// which is too expensive, so get approximated size depending on duration and definition params
|
||||
func (yt *YouTubeBuilder) getSize(duration int64, feed *model.Feed) int64 {
|
||||
if feed.Format == api.FormatAudio {
|
||||
if feed.Quality == api.QualityHigh {
|
||||
func (yt *YouTubeBuilder) getSize(duration int64, cfg *config.Feed) int64 {
|
||||
if cfg.Format == config.FormatAudio {
|
||||
if cfg.Quality == config.QualityHigh {
|
||||
return highAudioBytesPerSecond * duration
|
||||
}
|
||||
|
||||
@@ -269,7 +260,7 @@ func (yt *YouTubeBuilder) getSize(duration int64, feed *model.Feed) int64 {
|
||||
|
||||
// Video format
|
||||
|
||||
if feed.Quality == api.QualityHigh {
|
||||
if cfg.Quality == config.QualityHigh {
|
||||
return duration * hdBytesPerSecond
|
||||
}
|
||||
|
||||
@@ -278,7 +269,7 @@ func (yt *YouTubeBuilder) getSize(duration int64, feed *model.Feed) int64 {
|
||||
|
||||
// Cost: 5 units (call: 1, snippet: 2, contentDetails: 2)
|
||||
// See https://developers.google.com/youtube/v3/docs/videos/list#part
|
||||
func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.PlaylistItemSnippet, feed *model.Feed) error {
|
||||
func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.PlaylistItemSnippet, feed *Feed, cfg *config.Feed) error {
|
||||
// Make the list of video ids
|
||||
ids := make([]string, 0, len(playlist))
|
||||
for _, s := range playlist {
|
||||
@@ -295,7 +286,7 @@ func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.Pl
|
||||
snippet = video.Snippet
|
||||
videoID = video.Id
|
||||
videoURL = fmt.Sprintf("https://youtube.com/watch?v=%s", video.Id)
|
||||
image = yt.selectThumbnail(snippet.Thumbnails, feed.Quality, videoID)
|
||||
image = yt.selectThumbnail(snippet.Thumbnails, cfg.Quality, videoID)
|
||||
)
|
||||
|
||||
// Parse date added to playlist / publication date
|
||||
@@ -326,10 +317,10 @@ func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.Pl
|
||||
|
||||
var (
|
||||
order = strconv.FormatInt(playlistItem.Position, 10)
|
||||
size = yt.getSize(seconds, feed)
|
||||
size = yt.getSize(seconds, cfg)
|
||||
)
|
||||
|
||||
feed.Episodes = append(feed.Episodes, &model.Item{
|
||||
feed.Episodes = append(feed.Episodes, &Item{
|
||||
ID: video.Id,
|
||||
Title: snippet.Title,
|
||||
Description: snippet.Description,
|
||||
@@ -337,10 +328,8 @@ func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.Pl
|
||||
Duration: seconds,
|
||||
Size: size,
|
||||
VideoURL: videoURL,
|
||||
PubDate: model.Timestamp(pubDate),
|
||||
|
||||
// Need for sorting
|
||||
Order: order,
|
||||
PubDate: pubDate,
|
||||
Order: order,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,14 +337,14 @@ func (yt *YouTubeBuilder) queryVideoDescriptions(playlist map[string]*youtube.Pl
|
||||
}
|
||||
|
||||
// Cost: (3 units + 5 units) * X pages = 8 units per page
|
||||
func (yt *YouTubeBuilder) queryItems(itemID string, feed *model.Feed) error {
|
||||
func (yt *YouTubeBuilder) queryItems(feed *Feed, cfg *config.Feed) error {
|
||||
var (
|
||||
token string
|
||||
count int
|
||||
)
|
||||
|
||||
for {
|
||||
items, pageToken, err := yt.listPlaylistItems(itemID, token)
|
||||
items, pageToken, err := yt.listPlaylistItems(feed.ItemID, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -374,49 +363,39 @@ func (yt *YouTubeBuilder) queryItems(itemID string, feed *model.Feed) error {
|
||||
}
|
||||
|
||||
// Query video descriptions from the list of ids
|
||||
if err := yt.queryVideoDescriptions(snippets, feed); err != nil {
|
||||
if err := yt.queryVideoDescriptions(snippets, feed, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count >= feed.PageSize || token == "" {
|
||||
if count >= cfg.PageSize || token == "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (yt *YouTubeBuilder) Build(feed *model.Feed) error {
|
||||
feed.Episodes = []*model.Item{}
|
||||
func (yt *YouTubeBuilder) Build(cfg *config.Feed) (*Feed, error) {
|
||||
info, err := link.Parse(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Query general information about feed (title, description, lang, etc)
|
||||
itemID, err := yt.queryFeed(feed)
|
||||
feed, err := yt.queryFeed(&info, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get video descriptions
|
||||
if feed.PageSize == 0 {
|
||||
feed.PageSize = maxYoutubeResults
|
||||
if err := yt.queryItems(feed, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yt.queryItems(itemID, feed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// New interface
|
||||
|
||||
sort.Slice(feed.Episodes, func(i, j int) bool {
|
||||
item1, _ := strconv.Atoi(feed.Episodes[i].Order)
|
||||
item2, _ := strconv.Atoi(feed.Episodes[j].Order)
|
||||
return item1 < item2
|
||||
})
|
||||
|
||||
if len(feed.Episodes) > 0 {
|
||||
feed.LastID = feed.Episodes[0].ID
|
||||
} else {
|
||||
feed.LastID = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func NewYouTubeBuilder(key string) (*YouTubeBuilder, error) {
|
||||
@@ -0,0 +1,99 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/link"
|
||||
)
|
||||
|
||||
var ytKey = os.Getenv("YOUTUBE_TEST_API_KEY")
|
||||
|
||||
func TestYT_QueryChannel(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel, err := builder.listChannels(link.TypeChannel, "UC2yTVSttx7lxAOAzx1opjoA", "id")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UC2yTVSttx7lxAOAzx1opjoA", channel.Id)
|
||||
|
||||
channel, err = builder.listChannels(link.TypeUser, "fxigr1", "id")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UCr_fwF-n-2_olTYd-m3n32g", channel.Id)
|
||||
}
|
||||
|
||||
func TestYT_BuildFeed(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
urls := []string{
|
||||
"https://youtube.com/user/fxigr1",
|
||||
"https://www.youtube.com/channel/UCupvZG-5ko_eiXAupbDfxWw",
|
||||
"https://www.youtube.com/playlist?list=PLfVk3KMh3VX1yJShGRsJmsqAjvMIviJYQ",
|
||||
"https://www.youtube.com/channel/UCK9lZ2lHRBgx2LOcqPifukA",
|
||||
"https://youtube.com/user/WylsaLive",
|
||||
"https://www.youtube.com/playlist?list=PLUVl5pafUrBydT_gsCjRGeCy0hFHloec8",
|
||||
}
|
||||
|
||||
for _, addr := range urls {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
feed, err := builder.Build(&config.Feed{URL: addr})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, feed.Title)
|
||||
assert.NotEmpty(t, feed.Description)
|
||||
assert.NotEmpty(t, feed.Author)
|
||||
assert.NotEmpty(t, feed.ItemURL)
|
||||
|
||||
assert.NotZero(t, len(feed.Episodes))
|
||||
|
||||
for _, item := range feed.Episodes {
|
||||
assert.NotEmpty(t, item.Title)
|
||||
assert.NotEmpty(t, item.VideoURL)
|
||||
assert.NotZero(t, item.Duration)
|
||||
|
||||
assert.NotEmpty(t, item.Title)
|
||||
assert.NotEmpty(t, item.Thumbnail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYT_GetVideoCount(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
feeds := []*link.Info{
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypeUser, ItemID: "fxigr1"},
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypeChannel, ItemID: "UCupvZG-5ko_eiXAupbDfxWw"},
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypePlaylist, ItemID: "PLfVk3KMh3VX1yJShGRsJmsqAjvMIviJYQ"},
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypeChannel, ItemID: "UCK9lZ2lHRBgx2LOcqPifukA"},
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypeUser, ItemID: "WylsaLive"},
|
||||
{Provider: link.ProviderYoutube, LinkType: link.TypePlaylist, ItemID: "PLUVl5pafUrBydT_gsCjRGeCy0hFHloec8"},
|
||||
}
|
||||
|
||||
for _, f := range feeds {
|
||||
feed := f
|
||||
t.Run(f.ItemID, func(t *testing.T) {
|
||||
count, err := builder.GetVideoCount(feed)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package builders
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
var ytKey = os.Getenv("YOUTUBE_TEST_API_KEY")
|
||||
|
||||
func TestYT_QueryChannel(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel, err := builder.listChannels(api.LinkTypeChannel, "UC2yTVSttx7lxAOAzx1opjoA", "id")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UC2yTVSttx7lxAOAzx1opjoA", channel.Id)
|
||||
|
||||
channel, err = builder.listChannels(api.LinkTypeUser, "fxigr1", "id")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UCr_fwF-n-2_olTYd-m3n32g", channel.Id)
|
||||
}
|
||||
|
||||
func TestYT_BuildFeed(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
feeds := []*model.Feed{
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeUser, ItemID: "fxigr1"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeChannel, ItemID: "UCupvZG-5ko_eiXAupbDfxWw"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypePlaylist, ItemID: "PLfVk3KMh3VX1yJShGRsJmsqAjvMIviJYQ"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeChannel, ItemID: "UCK9lZ2lHRBgx2LOcqPifukA"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeUser, ItemID: "WylsaLive"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypePlaylist, ItemID: "PLUVl5pafUrBydT_gsCjRGeCy0hFHloec8"},
|
||||
}
|
||||
|
||||
for _, f := range feeds {
|
||||
feed := f
|
||||
t.Run(feed.ItemID, func(t *testing.T) {
|
||||
err := builder.Build(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, feed.Title)
|
||||
assert.NotEmpty(t, feed.Description)
|
||||
assert.NotEmpty(t, feed.Author)
|
||||
assert.NotEmpty(t, feed.ItemURL)
|
||||
assert.NotEmpty(t, feed.LastID)
|
||||
|
||||
assert.NotZero(t, len(feed.Episodes))
|
||||
|
||||
for _, item := range feed.Episodes {
|
||||
assert.NotEmpty(t, item.Title)
|
||||
assert.NotEmpty(t, item.VideoURL)
|
||||
assert.NotZero(t, item.Duration)
|
||||
|
||||
assert.NotEmpty(t, item.Title)
|
||||
assert.NotEmpty(t, item.Thumbnail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYT_GetVideoCount(t *testing.T) {
|
||||
if ytKey == "" {
|
||||
t.Skip("YouTube API key is not provided")
|
||||
}
|
||||
|
||||
builder, err := NewYouTubeBuilder(ytKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
feeds := []*model.Feed{
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeUser, ItemID: "fxigr1"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeChannel, ItemID: "UCupvZG-5ko_eiXAupbDfxWw"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypePlaylist, ItemID: "PLfVk3KMh3VX1yJShGRsJmsqAjvMIviJYQ"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeChannel, ItemID: "UCK9lZ2lHRBgx2LOcqPifukA"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypeUser, ItemID: "WylsaLive"},
|
||||
{Provider: api.ProviderYoutube, LinkType: api.LinkTypePlaylist, ItemID: "PLUVl5pafUrBydT_gsCjRGeCy0hFHloec8"},
|
||||
}
|
||||
|
||||
for _, f := range feeds {
|
||||
feed := f
|
||||
t.Run(f.ItemID, func(t *testing.T) {
|
||||
count, err := builder.GetVideoCount(feed)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
+59
-5
@@ -7,6 +7,29 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Quality to use when downloading episodes
|
||||
type Quality string
|
||||
|
||||
const (
|
||||
QualityHigh = Quality("high")
|
||||
QualityLow = Quality("low")
|
||||
)
|
||||
|
||||
// Format to convert episode when downloading episodes
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatAudio = Format("audio")
|
||||
FormatVideo = Format("video")
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFormat = FormatVideo
|
||||
DefaultQuality = QualityHigh
|
||||
DefaultPageSize = 50
|
||||
DefaultUpdatePeriod = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Feed is a configuration for a feed
|
||||
type Feed struct {
|
||||
// URL is a full URL of the field
|
||||
@@ -19,6 +42,12 @@ type Feed struct {
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
// NOTE: too often update check might drain your API token.
|
||||
UpdatePeriod Duration `toml:"update_period"`
|
||||
// Quality to use for this feed
|
||||
Quality Quality `toml:"quality"`
|
||||
// Format to use for this feed
|
||||
Format Format `toml:"format"`
|
||||
// Custom image to use
|
||||
CoverArt string `toml:"cover_art"`
|
||||
}
|
||||
|
||||
type Tokens struct {
|
||||
@@ -30,14 +59,20 @@ type Tokens struct {
|
||||
Vimeo string `toml:"vimeo"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// DataDir is a path to a directory to keep XML feeds and downloaded episodes
|
||||
DataDir string `toml:"data_dir"`
|
||||
type Server struct {
|
||||
// Port is a server port to listen to
|
||||
Port int `toml:"port"`
|
||||
// DataDir is a path to a directory to keep XML feeds and downloaded episodes,
|
||||
// that will be available to user via web server for download.
|
||||
DataDir string `toml:"data_dir"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Server is the web server configuration
|
||||
Server Server `toml:"server"`
|
||||
// Feeds is a list of feeds to host by this app.
|
||||
// ID will be used as feed ID in http://podsync.net/{FEED_ID}.xml
|
||||
Feeds map[string]Feed
|
||||
Feeds map[string]*Feed
|
||||
// Tokens is API keys to use to access YouTube/Vimeo APIs.
|
||||
Tokens Tokens `toml:"tokens"`
|
||||
}
|
||||
@@ -47,7 +82,26 @@ func LoadConfig(path string) (*Config, error) {
|
||||
config := Config{}
|
||||
_, err := toml.DecodeFile(path, &config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to load config file %q", path)
|
||||
return nil, errors.Wrap(err, "failed to load config file")
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
for _, feed := range config.Feeds {
|
||||
if feed.UpdatePeriod.Duration == 0 {
|
||||
feed.UpdatePeriod.Duration = DefaultUpdatePeriod
|
||||
}
|
||||
|
||||
if feed.Quality == "" {
|
||||
feed.Quality = DefaultQuality
|
||||
}
|
||||
|
||||
if feed.Format == "" {
|
||||
feed.Format = DefaultFormat
|
||||
}
|
||||
|
||||
if feed.PageSize == 0 {
|
||||
feed.PageSize = DefaultPageSize
|
||||
}
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
|
||||
@@ -12,18 +12,21 @@ import (
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
const file = `
|
||||
data_dir = "test/data/"
|
||||
port = 80
|
||||
|
||||
[tokens]
|
||||
youtube = "123"
|
||||
vimeo = "321"
|
||||
|
||||
[server]
|
||||
port = 80
|
||||
data_dir = "test/data/"
|
||||
|
||||
[feeds]
|
||||
[feeds.XYZ]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
page_size = 50
|
||||
update_period = "5h"
|
||||
format = "audio"
|
||||
quality = "low"
|
||||
`
|
||||
|
||||
f, err := ioutil.TempFile("", "")
|
||||
@@ -38,8 +41,8 @@ vimeo = "321"
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
assert.Equal(t, "test/data/", config.DataDir)
|
||||
assert.EqualValues(t, 80, config.Port)
|
||||
assert.Equal(t, "test/data/", config.Server.DataDir)
|
||||
assert.EqualValues(t, 80, config.Server.Port)
|
||||
|
||||
assert.Equal(t, "123", config.Tokens.YouTube)
|
||||
assert.Equal(t, "321", config.Tokens.Vimeo)
|
||||
@@ -50,4 +53,34 @@ vimeo = "321"
|
||||
assert.Equal(t, "https://youtube.com/watch?v=ygIUF678y40", feed.URL)
|
||||
assert.EqualValues(t, 50, feed.PageSize)
|
||||
assert.EqualValues(t, Duration{5 * time.Hour}, feed.UpdatePeriod)
|
||||
assert.EqualValues(t, "audio", feed.Format)
|
||||
assert.EqualValues(t, "low", feed.Quality)
|
||||
}
|
||||
|
||||
func TestApplyDefaults(t *testing.T) {
|
||||
const file = `
|
||||
[feeds]
|
||||
[feeds.A]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
`
|
||||
f, err := ioutil.TempFile("", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err = f.WriteString(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
config, err := LoadConfig(f.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
assert.Len(t, config.Feeds, 1)
|
||||
feed, ok := config.Feeds["A"]
|
||||
require.True(t, ok)
|
||||
|
||||
assert.EqualValues(t, feed.UpdatePeriod, Duration{DefaultUpdatePeriod})
|
||||
assert.EqualValues(t, feed.PageSize, 50)
|
||||
assert.EqualValues(t, feed.Quality, "high")
|
||||
assert.EqualValues(t, feed.Format, "video")
|
||||
}
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/link"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -78,11 +79,13 @@ func (s *Service) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity)
|
||||
}
|
||||
|
||||
func (s *Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*model.Feed, error) {
|
||||
feed, err := parseURL(req.URL)
|
||||
_, err := link.Parse(req.URL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to create feed for URL: %s", req.URL)
|
||||
}
|
||||
|
||||
feed := &model.Feed{}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
feed.UserID = identity.UserID
|
||||
|
||||
@@ -27,6 +27,8 @@ var feed = &model.Feed{
|
||||
}
|
||||
|
||||
func TestService_CreateFeed(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package link
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeChannel = Type("channel")
|
||||
TypePlaylist = Type("playlist")
|
||||
TypeUser = Type("user")
|
||||
TypeGroup = Type("group")
|
||||
)
|
||||
|
||||
type Provider string
|
||||
|
||||
const (
|
||||
ProviderYoutube = Provider("youtube")
|
||||
ProviderVimeo = Provider("vimeo")
|
||||
)
|
||||
|
||||
// Info represents data extracted from URL
|
||||
type Info struct {
|
||||
LinkType Type // Either group, channel or user
|
||||
Provider Provider // Youtube or Vimeo
|
||||
ItemID string
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package feeds
|
||||
package link
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func parseURL(link string) (*model.Feed, error) {
|
||||
func Parse(link string) (Info, error) {
|
||||
if !strings.HasPrefix(link, "http") {
|
||||
link = "https://" + link
|
||||
}
|
||||
@@ -17,47 +15,47 @@ func parseURL(link string) (*model.Feed, error) {
|
||||
parsed, err := url.Parse(link)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to parse url: %s", link)
|
||||
return nil, err
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
feed := &model.Feed{}
|
||||
info := Info{}
|
||||
|
||||
if strings.HasSuffix(parsed.Host, "youtube.com") {
|
||||
kind, id, err := parseYoutubeURL(parsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
feed.Provider = api.ProviderYoutube
|
||||
feed.LinkType = kind
|
||||
feed.ItemID = id
|
||||
info.Provider = ProviderYoutube
|
||||
info.LinkType = kind
|
||||
info.ItemID = id
|
||||
|
||||
return feed, nil
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(parsed.Host, "vimeo.com") {
|
||||
kind, id, err := parseVimeoURL(parsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
feed.Provider = api.ProviderVimeo
|
||||
feed.LinkType = kind
|
||||
feed.ItemID = id
|
||||
info.Provider = ProviderVimeo
|
||||
info.LinkType = kind
|
||||
info.ItemID = id
|
||||
|
||||
return feed, nil
|
||||
return info, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported URL host")
|
||||
return Info{}, errors.New("unsupported URL host")
|
||||
}
|
||||
|
||||
func parseYoutubeURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
func parseYoutubeURL(parsed *url.URL) (Type, string, error) {
|
||||
path := parsed.EscapedPath()
|
||||
|
||||
// https://www.youtube.com/playlist?list=PLCB9F975ECF01953C
|
||||
// https://www.youtube.com/watch?v=rbCbho7aLYw&list=PLMpEfaKcGjpWEgNtdnsvLX6LzQL0UC0EM
|
||||
if strings.HasPrefix(path, "/playlist") || strings.HasPrefix(path, "/watch") {
|
||||
kind := api.LinkTypePlaylist
|
||||
kind := TypePlaylist
|
||||
|
||||
id := parsed.Query().Get("list")
|
||||
if id != "" {
|
||||
@@ -70,7 +68,7 @@ func parseYoutubeURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
// - https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og
|
||||
// - https://www.youtube.com/channel/UCrlakW-ewUT8sOod6Wmzyow/videos
|
||||
if strings.HasPrefix(path, "/channel") {
|
||||
kind := api.LinkTypeChannel
|
||||
kind := TypeChannel
|
||||
parts := strings.Split(parsed.EscapedPath(), "/")
|
||||
if len(parts) <= 2 {
|
||||
return "", "", errors.New("invalid youtube channel link")
|
||||
@@ -86,7 +84,7 @@ func parseYoutubeURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
|
||||
// - https://www.youtube.com/user/fxigr1
|
||||
if strings.HasPrefix(path, "/user") {
|
||||
kind := api.LinkTypeUser
|
||||
kind := TypeUser
|
||||
|
||||
parts := strings.Split(parsed.EscapedPath(), "/")
|
||||
if len(parts) <= 2 {
|
||||
@@ -104,23 +102,23 @@ func parseYoutubeURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
return "", "", errors.New("unsupported link format")
|
||||
}
|
||||
|
||||
func parseVimeoURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
func parseVimeoURL(parsed *url.URL) (Type, string, error) {
|
||||
parts := strings.Split(parsed.EscapedPath(), "/")
|
||||
if len(parts) <= 1 {
|
||||
return "", "", errors.New("invalid vimeo link path")
|
||||
}
|
||||
|
||||
var kind api.LinkType
|
||||
var kind Type
|
||||
switch parts[1] {
|
||||
case "groups":
|
||||
kind = api.LinkTypeGroup
|
||||
kind = TypeGroup
|
||||
case "channels":
|
||||
kind = api.LinkTypeChannel
|
||||
kind = TypeChannel
|
||||
default:
|
||||
kind = api.LinkTypeUser
|
||||
kind = TypeUser
|
||||
}
|
||||
|
||||
if kind == api.LinkTypeGroup || kind == api.LinkTypeChannel {
|
||||
if kind == TypeGroup || kind == TypeChannel {
|
||||
if len(parts) <= 2 {
|
||||
return "", "", errors.New("invalid channel link")
|
||||
}
|
||||
@@ -133,7 +131,7 @@ func parseVimeoURL(parsed *url.URL) (api.LinkType, string, error) {
|
||||
return kind, id, nil
|
||||
}
|
||||
|
||||
if kind == api.LinkTypeUser {
|
||||
if kind == TypeUser {
|
||||
id := parts[1]
|
||||
if id == "" {
|
||||
return "", "", errors.New("invalid id")
|
||||
@@ -1,25 +1,23 @@
|
||||
package feeds
|
||||
package link
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
)
|
||||
|
||||
func TestParseYoutubeURL_Playlist(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://www.youtube.com/playlist?list=PLCB9F975ECF01953C")
|
||||
kind, id, err := parseYoutubeURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypePlaylist, kind)
|
||||
require.Equal(t, TypePlaylist, kind)
|
||||
require.Equal(t, "PLCB9F975ECF01953C", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("https://www.youtube.com/watch?v=rbCbho7aLYw&list=PLMpEfaKcGjpWEgNtdnsvLX6LzQL0UC0EM")
|
||||
kind, id, err = parseYoutubeURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypePlaylist, kind)
|
||||
require.Equal(t, TypePlaylist, kind)
|
||||
require.Equal(t, "PLMpEfaKcGjpWEgNtdnsvLX6LzQL0UC0EM", id)
|
||||
}
|
||||
|
||||
@@ -27,13 +25,13 @@ func TestParseYoutubeURL_Channel(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og")
|
||||
kind, id, err := parseYoutubeURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeChannel, kind)
|
||||
require.Equal(t, TypeChannel, kind)
|
||||
require.Equal(t, "UC5XPnUk8Vvv_pWslhwom6Og", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("https://www.youtube.com/channel/UCrlakW-ewUT8sOod6Wmzyow/videos")
|
||||
kind, id, err = parseYoutubeURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeChannel, kind)
|
||||
require.Equal(t, TypeChannel, kind)
|
||||
require.Equal(t, "UCrlakW-ewUT8sOod6Wmzyow", id)
|
||||
}
|
||||
|
||||
@@ -41,7 +39,7 @@ func TestParseYoutubeURL_User(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://youtube.com/user/fxigr1")
|
||||
kind, id, err := parseYoutubeURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeUser, kind)
|
||||
require.Equal(t, TypeUser, kind)
|
||||
require.Equal(t, "fxigr1", id)
|
||||
}
|
||||
|
||||
@@ -59,25 +57,25 @@ func TestParseVimeoURL_Group(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://vimeo.com/groups/109")
|
||||
kind, id, err := parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeGroup, kind)
|
||||
require.Equal(t, TypeGroup, kind)
|
||||
require.Equal(t, "109", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("http://vimeo.com/groups/109")
|
||||
kind, id, err = parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeGroup, kind)
|
||||
require.Equal(t, TypeGroup, kind)
|
||||
require.Equal(t, "109", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("http://www.vimeo.com/groups/109")
|
||||
kind, id, err = parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeGroup, kind)
|
||||
require.Equal(t, TypeGroup, kind)
|
||||
require.Equal(t, "109", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("https://vimeo.com/groups/109/videos/")
|
||||
kind, id, err = parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeGroup, kind)
|
||||
require.Equal(t, TypeGroup, kind)
|
||||
require.Equal(t, "109", id)
|
||||
}
|
||||
|
||||
@@ -85,13 +83,13 @@ func TestParseVimeoURL_Channel(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://vimeo.com/channels/staffpicks")
|
||||
kind, id, err := parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeChannel, kind)
|
||||
require.Equal(t, TypeChannel, kind)
|
||||
require.Equal(t, "staffpicks", id)
|
||||
|
||||
link, _ = url.ParseRequestURI("http://vimeo.com/channels/staffpicks/146224925")
|
||||
kind, id, err = parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeChannel, kind)
|
||||
require.Equal(t, TypeChannel, kind)
|
||||
require.Equal(t, "staffpicks", id)
|
||||
}
|
||||
|
||||
@@ -99,7 +97,7 @@ func TestParseVimeoURL_User(t *testing.T) {
|
||||
link, _ := url.ParseRequestURI("https://vimeo.com/awhitelabelproduct")
|
||||
kind, id, err := parseVimeoURL(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.LinkTypeUser, kind)
|
||||
require.Equal(t, TypeUser, kind)
|
||||
require.Equal(t, "awhitelabelproduct", id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user