├── .gitignore ├── .idea ├── freeflix.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── doc └── screenshots │ ├── dialog.png │ └── movies.png ├── main.go ├── server.go ├── service └── yts.go └── torrent ├── client.go ├── fileEntry.go ├── monitoring.go └── templates └── status.html /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | Movies/ 3 | public/ 4 | go_build_freeflix.exe -------------------------------------------------------------------------------- /.idea/freeflix.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:stretch 2 | COPY . $GOPATH/src/freeflix 3 | EXPOSE 8080 4 | ADD https://github.com/ninjaintrouble/freeflix-frontend/releases/download/1.0/frontend.tar $GOPATH/bin 5 | WORKDIR $GOPATH/src/freeflix 6 | RUN apt-get update &&\ 7 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh &&\ 8 | dep ensure &&\ 9 | apt-get install gcc &&\ 10 | go install -i -v 11 | WORKDIR $GOPATH/bin 12 | RUN mkdir -p ./torrent/templates &&\ 13 | cp ./../src/freeflix/torrent/templates/status.html ./torrent/templates/status.html &&\ 14 | tar -xvf frontend.tar 15 | CMD ["freeflix"] -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:071bc10aade52ea2b6f919474e18900671903b24b12074b9bcc1c17698035586" 6 | name = "github.com/RoaringBitmap/roaring" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "4c23670306840b0f8755db247d2e9b8369fc356e" 10 | version = "v0.4.15" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:fce3735c805fdd8f5a5b646eccfcb410eca0de92399d7de02812eb7a308ece20" 15 | name = "github.com/anacrolix/dht" 16 | packages = [ 17 | ".", 18 | "krpc", 19 | ] 20 | pruneopts = "UT" 21 | revision = "5f03eb1264917b71e6e8432243e8a45d5b1e7c81" 22 | 23 | [[projects]] 24 | branch = "master" 25 | digest = "1:a7c01fac12978ab4faf942a1463d566f6e626a6b8cba7d7f7c69830049ec75a4" 26 | name = "github.com/anacrolix/go-libutp" 27 | packages = ["."] 28 | pruneopts = "UT" 29 | revision = "34b43d88094047b1c044ebf3a1810db35fffd454" 30 | 31 | [[projects]] 32 | branch = "master" 33 | digest = "1:686a37f81b55b2e4813e90a1e3d41a84591b768c8bc5bd4027ce03db11d29a67" 34 | name = "github.com/anacrolix/log" 35 | packages = ["."] 36 | pruneopts = "UT" 37 | revision = "74f61a49b0a4aa9ede7dd278395e7a9729804dde" 38 | 39 | [[projects]] 40 | branch = "master" 41 | digest = "1:380c59a36cf5959ca36b69a163c629b57889f98c3394aa1317458be0345c4acd" 42 | name = "github.com/anacrolix/missinggo" 43 | packages = [ 44 | ".", 45 | "bitmap", 46 | "expect", 47 | "httptoo", 48 | "inproc", 49 | "iter", 50 | "mime", 51 | "orderedmap", 52 | "perf", 53 | "pproffd", 54 | "prioritybitmap", 55 | "pubsub", 56 | "resource", 57 | "slices", 58 | "x", 59 | ] 60 | pruneopts = "UT" 61 | revision = "60ef2fbf63df5d871ada2680d4d8a6013dcd1745" 62 | 63 | [[projects]] 64 | branch = "master" 65 | digest = "1:da88a5f13467186684abd16aa6f85544af6243a9111bf0d0677a92788a15ec7a" 66 | name = "github.com/anacrolix/mmsg" 67 | packages = [ 68 | ".", 69 | "socket", 70 | ] 71 | pruneopts = "UT" 72 | revision = "a4a3ba1fc8bb6400db01c47508156bf9c67156c6" 73 | 74 | [[projects]] 75 | branch = "master" 76 | digest = "1:c9444b5e8f5df73cb6653af4b71fc42335f4f117d944b74dfdaee360ff1d30a8" 77 | name = "github.com/anacrolix/sync" 78 | packages = ["."] 79 | pruneopts = "UT" 80 | revision = "fda11526ff0853fb50738fd7ba5bc9af2eb016bf" 81 | 82 | [[projects]] 83 | branch = "master" 84 | digest = "1:82002fd8913ba8074ab0f72d9625b1f27f77de48496de43c617ed35c140cc5ae" 85 | name = "github.com/anacrolix/torrent" 86 | packages = [ 87 | ".", 88 | "bencode", 89 | "iplist", 90 | "logonce", 91 | "metainfo", 92 | "mmap_span", 93 | "mse", 94 | "peer_protocol", 95 | "storage", 96 | "tracker", 97 | ] 98 | pruneopts = "UT" 99 | revision = "81e6061a5347c35e10b83b9c969811c6f46e338e" 100 | 101 | [[projects]] 102 | branch = "master" 103 | digest = "1:c8c563c60665dd4b185e3c042d12e915b3dd6129d5477ae313914dce59c7319d" 104 | name = "github.com/anacrolix/utp" 105 | packages = ["."] 106 | pruneopts = "UT" 107 | revision = "9e0e1d1d0572f5a09a669d4ea0372d8e8078dcc9" 108 | 109 | [[projects]] 110 | digest = "1:0f98f59e9a2f4070d66f0c9c39561f68fcd1dc837b22a852d28d0003aebd1b1e" 111 | name = "github.com/boltdb/bolt" 112 | packages = ["."] 113 | pruneopts = "UT" 114 | revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8" 115 | version = "v1.3.1" 116 | 117 | [[projects]] 118 | branch = "master" 119 | digest = "1:7bfd6f49553eb9409ecde1d0311fdcc4944a887d9fff23415f74b7bd462d95a4" 120 | name = "github.com/bradfitz/iter" 121 | packages = ["."] 122 | pruneopts = "UT" 123 | revision = "454541ec3da2a73fc34fd049b19ee5777bf19345" 124 | 125 | [[projects]] 126 | digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" 127 | name = "github.com/davecgh/go-spew" 128 | packages = ["spew"] 129 | pruneopts = "UT" 130 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 131 | version = "v1.1.0" 132 | 133 | [[projects]] 134 | branch = "master" 135 | digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74" 136 | name = "github.com/dustin/go-humanize" 137 | packages = ["."] 138 | pruneopts = "UT" 139 | revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e" 140 | 141 | [[projects]] 142 | branch = "master" 143 | digest = "1:67d0b50be0549e610017cb91e0b0b745ec0cad7c613bc8e18ff2d1c1fc8825a7" 144 | name = "github.com/edsrzf/mmap-go" 145 | packages = ["."] 146 | pruneopts = "UT" 147 | revision = "0bce6a6887123b67a60366d2c9fe2dfb74289d2e" 148 | 149 | [[projects]] 150 | branch = "master" 151 | digest = "1:87d8f1070e73e3dacde758c0377c1f4cd8ded02feab0208d1b83d40f90aa615a" 152 | name = "github.com/elgatito/upnp" 153 | packages = ["."] 154 | pruneopts = "UT" 155 | revision = "2f244d205f9a45e5e5c7ecd79c8d67c656e682a0" 156 | 157 | [[projects]] 158 | branch = "master" 159 | digest = "1:981722e5ee549ef844285c2b370c19e58b7dc30bc19ba3537f3df284b551b54a" 160 | name = "github.com/glycerine/go-unsnap-stream" 161 | packages = ["."] 162 | pruneopts = "UT" 163 | revision = "9f0cb55181dd3a0a4c168d3dbc72d4aca4853126" 164 | 165 | [[projects]] 166 | branch = "master" 167 | digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009" 168 | name = "github.com/golang/snappy" 169 | packages = ["."] 170 | pruneopts = "UT" 171 | revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" 172 | 173 | [[projects]] 174 | branch = "master" 175 | digest = "1:9887333bbef17574b1db5f9893ea137ac44107235d624408a3ac9e0b98fbb2cb" 176 | name = "github.com/google/btree" 177 | packages = ["."] 178 | pruneopts = "UT" 179 | revision = "e89373fe6b4a7413d7acd6da1725b83ef713e6e4" 180 | 181 | [[projects]] 182 | digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" 183 | name = "github.com/gorilla/context" 184 | packages = ["."] 185 | pruneopts = "UT" 186 | revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" 187 | version = "v1.1.1" 188 | 189 | [[projects]] 190 | digest = "1:664d37ea261f0fc73dd17f4a1f5f46d01fbb0b0d75f6375af064824424109b7d" 191 | name = "github.com/gorilla/handlers" 192 | packages = ["."] 193 | pruneopts = "UT" 194 | revision = "7e0847f9db758cdebd26c149d0ae9d5d0b9c98ce" 195 | version = "v1.4.0" 196 | 197 | [[projects]] 198 | digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" 199 | name = "github.com/gorilla/mux" 200 | packages = ["."] 201 | pruneopts = "UT" 202 | revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" 203 | version = "v1.6.2" 204 | 205 | [[projects]] 206 | digest = "1:6b1e7618931d0ed9a17fec1c1d84271907aa61172c50b8272327f6cd4fafb650" 207 | name = "github.com/huandu/xstrings" 208 | packages = ["."] 209 | pruneopts = "UT" 210 | revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1" 211 | version = "v1.0.0" 212 | 213 | [[projects]] 214 | digest = "1:3cafc6a5a1b8269605d9df4c6956d43d8011fc57f266ca6b9d04da6c09dee548" 215 | name = "github.com/mattn/go-sqlite3" 216 | packages = ["."] 217 | pruneopts = "UT" 218 | revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4" 219 | version = "v1.9.0" 220 | 221 | [[projects]] 222 | branch = "master" 223 | digest = "1:c22fdadfe2de0e1df75987cac34f1c721157dc24f180c76b4ef66a3c9637a799" 224 | name = "github.com/mschoch/smat" 225 | packages = ["."] 226 | pruneopts = "UT" 227 | revision = "90eadee771aeab36e8bf796039b8c261bebebe4f" 228 | 229 | [[projects]] 230 | digest = "1:5b3b29ce0e569f62935d9541dff2e16cc09df981ebde48e82259076a73a3d0c7" 231 | name = "github.com/op/go-logging" 232 | packages = ["."] 233 | pruneopts = "UT" 234 | revision = "b2cb9fa56473e98db8caba80237377e83fe44db5" 235 | version = "v1" 236 | 237 | [[projects]] 238 | branch = "master" 239 | digest = "1:3bf17a6e6eaa6ad24152148a631d18662f7212e21637c2699bff3369b7f00fa2" 240 | name = "github.com/petar/GoLLRB" 241 | packages = ["llrb"] 242 | pruneopts = "UT" 243 | revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" 244 | 245 | [[projects]] 246 | digest = "1:5e73b34a27d827212102605789de00bd411b2e434812133c83935fe9897c75e1" 247 | name = "github.com/philhofer/fwd" 248 | packages = ["."] 249 | pruneopts = "UT" 250 | revision = "bb6d471dc95d4fe11e432687f8b70ff496cf3136" 251 | version = "v1.0.0" 252 | 253 | [[projects]] 254 | digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" 255 | name = "github.com/pkg/errors" 256 | packages = ["."] 257 | pruneopts = "UT" 258 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 259 | version = "v0.8.0" 260 | 261 | [[projects]] 262 | branch = "master" 263 | digest = "1:8bc4ea931d51467ab9dd8c1fb6213625e7ac1de24a4883e992a1b0b713a8d1eb" 264 | name = "github.com/ryszard/goskiplist" 265 | packages = ["skiplist"] 266 | pruneopts = "UT" 267 | revision = "2dfbae5fcf46374f166f8969cb07e167f1be6273" 268 | 269 | [[projects]] 270 | digest = "1:d867dfa6751c8d7a435821ad3b736310c2ed68945d05b50fb9d23aee0540c8cc" 271 | name = "github.com/sirupsen/logrus" 272 | packages = ["."] 273 | pruneopts = "UT" 274 | revision = "3e01752db0189b9157070a0e1668a620f9a85da2" 275 | version = "v1.0.6" 276 | 277 | [[projects]] 278 | digest = "1:919bb3aa6d9d0b67648c219fa4925312bc3c2872da19e818fa769e9c97a2b643" 279 | name = "github.com/spaolacci/murmur3" 280 | packages = ["."] 281 | pruneopts = "UT" 282 | revision = "9f5d223c60793748f04a9d5b4b4eacddfc1f755d" 283 | version = "v1.1" 284 | 285 | [[projects]] 286 | digest = "1:eaa6698f44de8f2977e93c9b946e60a8af75f565058658aad2df8032b55c84e5" 287 | name = "github.com/tinylib/msgp" 288 | packages = ["msgp"] 289 | pruneopts = "UT" 290 | revision = "b2b6a672cf1e5b90748f79b8b81fc8c5cf0571a1" 291 | version = "1.0.2" 292 | 293 | [[projects]] 294 | digest = "1:981fe66c332248711bafe3b5033e939cfd175e897e20a14905d4fcdc0fd68f59" 295 | name = "github.com/willf/bitset" 296 | packages = ["."] 297 | pruneopts = "UT" 298 | revision = "d860f346b89450988a379d7d705e83c58d1ea227" 299 | version = "v1.1.3" 300 | 301 | [[projects]] 302 | digest = "1:306c3d1c8a92c718566e815d0c98d58071465036ec7e9b543cc4d6ae10c1c983" 303 | name = "github.com/willf/bloom" 304 | packages = ["."] 305 | pruneopts = "UT" 306 | revision = "54e3b963ee1652b06c4562cb9b6020ebc6e36e59" 307 | version = "v2.0.3" 308 | 309 | [[projects]] 310 | branch = "master" 311 | digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8" 312 | name = "golang.org/x/crypto" 313 | packages = ["ssh/terminal"] 314 | pruneopts = "UT" 315 | revision = "f027049dab0ad238e394a753dba2d14753473a04" 316 | 317 | [[projects]] 318 | branch = "master" 319 | digest = "1:72c614df418a5c5c10c9f162fa703d948334f13747117baffc509fe770603063" 320 | name = "golang.org/x/net" 321 | packages = [ 322 | "context", 323 | "internal/socks", 324 | "proxy", 325 | ] 326 | pruneopts = "UT" 327 | revision = "3673e40ba22529d22c3fd7c93e97b0ce50fa7bdd" 328 | 329 | [[projects]] 330 | branch = "master" 331 | digest = "1:8742e6e73627b2877c3f723bc1823d5667ec59011242480309dc90fa862512aa" 332 | name = "golang.org/x/sys" 333 | packages = [ 334 | "unix", 335 | "windows", 336 | ] 337 | pruneopts = "UT" 338 | revision = "bd9dbc187b6e1dacfdd2722a87e83093c2d7bd6e" 339 | 340 | [[projects]] 341 | branch = "master" 342 | digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4" 343 | name = "golang.org/x/time" 344 | packages = ["rate"] 345 | pruneopts = "UT" 346 | revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" 347 | 348 | [solve-meta] 349 | analyzer-name = "dep" 350 | analyzer-version = 1 351 | input-imports = [ 352 | "github.com/anacrolix/torrent", 353 | "github.com/gorilla/handlers", 354 | "github.com/gorilla/mux", 355 | "github.com/sirupsen/logrus", 356 | ] 357 | solver-name = "gps-cdcl" 358 | solver-version = 1 359 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/anacrolix/torrent" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | 36 | [[constraint]] 37 | name = "github.com/gorilla/mux" 38 | version = "1.6.2" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 gtestault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Project 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/ninjaintrouble/freeflix)](https://goreportcard.com/report/github.com/ninjaintrouble/freeflix) 3 | 4 | Streaming server with an integrated BitTorrent client. The server can stream over http while downloading a torrent. 5 | 6 | ### Start with Docker 7 | clone the repo and inside the source folder build the docker image. 8 | 9 | ``` 10 | git clone git@github.com:gtestault/freeflix.git 11 | cd freeflix 12 | docker build -t freeflix . 13 | ``` 14 | 15 | then run a container and bind the port 8080 exposed from the image 16 | 17 | ```docker run -p 8080:8080 freeflix``` 18 | 19 | ### VPN 20 | 21 | Please configure your VPN before deploying. The BitTorrent client has no blocklist 22 | 23 | ### Screenshots 24 | 25 | ![movies](/doc/screenshots/movies.png "Movies Dashboard") 26 | 27 | ![filter](/doc/screenshots/dialog.png "Advanced Filter") 28 | -------------------------------------------------------------------------------- /doc/screenshots/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtestault/freeflix/b07f661ca13d66c49a0d77bcc445a3a16d8aee8b/doc/screenshots/dialog.png -------------------------------------------------------------------------------- /doc/screenshots/movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtestault/freeflix/b07f661ca13d66c49a0d77bcc445a3a16d8aee8b/doc/screenshots/movies.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "os" 6 | ) 7 | 8 | func init() { 9 | log.SetOutput(os.Stdout) 10 | log.SetLevel(log.DebugLevel) 11 | log.SetFormatter(&log.TextFormatter{}) 12 | } 13 | 14 | func main() { 15 | StartServer() 16 | } 17 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "freeflix/service" 8 | "freeflix/torrent" 9 | "github.com/gorilla/handlers" 10 | "github.com/gorilla/mux" 11 | log "github.com/sirupsen/logrus" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | ) 16 | 17 | var yts *service.Yts 18 | var client *torrent.Client 19 | 20 | func init() { 21 | yts = service.NewClientYTS() 22 | var err error 23 | client, err = torrent.NewClient() 24 | if err != nil { 25 | log.Fatalf(err.Error()) 26 | os.Exit(1) 27 | } 28 | } 29 | 30 | //hookedResponseWriter hijacks the behavior of the file server if it tries to return 404 31 | //we serve the index file instead so that the angular router handles routing. 32 | type hookedResponseWriter struct { 33 | http.ResponseWriter 34 | ignore bool 35 | } 36 | 37 | func (hrw *hookedResponseWriter) WriteHeader(status int) { 38 | if status == 404 { 39 | hrw.ignore = true 40 | indexFile, err := ioutil.ReadFile("./public/index.html") 41 | if err != nil { 42 | log.Error("HookedResponseWriter: couldn't read index.html %v", err) 43 | } 44 | b := bytes.NewBuffer(indexFile) 45 | hrw.ResponseWriter.Header().Set("Content-type", "text/html") 46 | hrw.ResponseWriter.WriteHeader(http.StatusOK) 47 | if _, err := b.WriteTo(hrw.ResponseWriter); err != nil { 48 | log.Error("HookedResponseWriter: couldn't send index.html to client %v", err) 49 | } 50 | } 51 | hrw.ResponseWriter.WriteHeader(status) 52 | } 53 | 54 | func (hrw *hookedResponseWriter) Write(p []byte) (int, error) { 55 | if hrw.ignore { 56 | return len(p), nil 57 | } 58 | return hrw.ResponseWriter.Write(p) 59 | } 60 | 61 | //NotFoundHook is a special HTTP handler using a hooked ResponseWriter instead of the default ResponseWriter. 62 | type NotFoundHook struct { 63 | h http.Handler 64 | } 65 | 66 | func (nfh NotFoundHook) ServeHTTP(w http.ResponseWriter, r *http.Request) { 67 | nfh.h.ServeHTTP(&hookedResponseWriter{ResponseWriter: w}, r) 68 | } 69 | 70 | //StartServer starts the freeflix Server. Serving static content and API. 71 | func StartServer() { 72 | r := mux.NewRouter() 73 | headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"}) 74 | originsOk := handlers.AllowedOrigins([]string{"*"}) 75 | methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE"}) 76 | r.HandleFunc("/api/yts", getYtsMovies).Methods("GET") 77 | r.HandleFunc("/api/movie/watch", client.GetFile) 78 | r.HandleFunc("/api/movie/request", client.MovieRequest).Methods("GET") 79 | r.HandleFunc("/api/movie/status", client.TorrentStatus) 80 | r.HandleFunc("/api/movie/delete", client.MovieDelete).Methods("DELETE") 81 | //TODO: Access Control 82 | r.HandleFunc("/monitoring/status", client.Status) 83 | r.PathPrefix("/").Handler(NotFoundHook{http.FileServer(http.Dir("./public/"))}) 84 | log.Debug("Listening on port 8080") 85 | if err := http.ListenAndServe(":8080", handlers.CORS(originsOk, headersOk, methodsOk)(r)); err != nil { 86 | panic(err) 87 | } 88 | } 89 | 90 | func getYtsMovies(w http.ResponseWriter, r *http.Request) { 91 | w.Header().Set("Content-Type", "application/json") 92 | //query is search term for movies 93 | query, _ := getParam(r, "query") 94 | //rating is minimum imdb 95 | rating, _ := getParam(r, "rating") 96 | page, _ := getParam(r, "page") 97 | sortBy, _ := getParam(r, "sort_by") 98 | orderBy, _ := getParam(r, "order_by") 99 | 100 | moviePage, err := yts.MoviePage(page, query, rating, sortBy, orderBy) 101 | if err != nil { 102 | http.Error(w, "yts service offline", http.StatusServiceUnavailable) 103 | log.Error(err) 104 | } 105 | err = json.NewEncoder(w).Encode(moviePage) 106 | if err != nil { 107 | log.WithError(err).Error("encoding YtsPage failed") 108 | http.Error(w, ":whale:", http.StatusInternalServerError) 109 | } 110 | } 111 | 112 | func getParam(r *http.Request, param string) (string, error) { 113 | packed, ok := r.URL.Query()[param] 114 | if !ok || len(packed) < 1 { 115 | return "", fmt.Errorf("getParam(%s): no infoHash in Request", param) 116 | } 117 | return packed[0], nil 118 | } 119 | -------------------------------------------------------------------------------- /service/yts.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | log "github.com/sirupsen/logrus" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | ) 11 | 12 | const ( 13 | endpointYTS = "https://yts.am/api/v2/" 14 | listMoviesYTS = "list_movies.json?" 15 | ) 16 | 17 | //Yts service from website https://yts.am/ 18 | type Yts struct { 19 | } 20 | 21 | func getYtsEndpoint() string { 22 | e := os.Getenv("YTS_ENDPOINT") 23 | if len(e) > 0 { 24 | return e 25 | } 26 | 27 | return endpointYTS 28 | } 29 | 30 | type ytsMoviePage struct { 31 | Status string 32 | Data struct { 33 | Movies []*YtsMovie 34 | } 35 | } 36 | 37 | //YtsMovie stores a Movie Object returned from the YTS API. 38 | type YtsMovie struct { 39 | Id int 40 | Url string 41 | ImdbCode string `json:"imdb_code"` 42 | Title string 43 | Year int 44 | Rating float32 45 | Runtime int 46 | Genres []string 47 | Summary string 48 | YTCode string `json:"yt_trailer_code"` 49 | Language string 50 | SmallCoverImage string `json:"small_cover_image"` 51 | MediumCoverImage string `json:"medium_cover_image"` 52 | LargeCoverImage string `json:"large_cover_image"` 53 | Torrents []*YtsTorrent 54 | } 55 | 56 | //YtsTorrent stores torrent information of a specific YTS movie. 57 | type YtsTorrent struct { 58 | Url string 59 | Hash string 60 | Quality string 61 | Seeds int 62 | Peers int 63 | Size string 64 | } 65 | 66 | //NewClientYTS creates a new YTS Service instance. 67 | func NewClientYTS() *Yts { 68 | return &Yts{} 69 | } 70 | 71 | //MoviePage gets a page of movies from the YTS API. 72 | func (Yts) MoviePage(page, query, rating, sortBy, orderBy string) ([]*YtsMovie, error) { 73 | v := url.Values{} 74 | if page != "" { 75 | v.Add("page", page) 76 | } 77 | if query != "" { 78 | v.Add("query_term", query) 79 | } 80 | if rating != "" { 81 | v.Add("minimum_rating", rating) 82 | } 83 | if sortBy != "" { 84 | v.Add("sort_by", sortBy) 85 | } 86 | if orderBy != "" { 87 | v.Add("order_by", orderBy) 88 | } 89 | reqURL := getYtsEndpoint() + listMoviesYTS + v.Encode() 90 | res, err := http.Get(reqURL) 91 | log.WithField("req", reqURL).Debug("Page Request to YTS") 92 | if err != nil { 93 | log.Error(err) 94 | return nil, err 95 | } 96 | 97 | dec := json.NewDecoder(res.Body) 98 | defer func() { 99 | err := res.Body.Close() 100 | if err != nil { 101 | log.Error(err) 102 | } 103 | }() 104 | var response ytsMoviePage 105 | err = dec.Decode(&response) 106 | if err != nil { 107 | return nil, fmt.Errorf("MoviePage: error decoding json YTS response: %v", err) 108 | } 109 | return response.Data.Movies, nil 110 | } 111 | -------------------------------------------------------------------------------- /torrent/client.go: -------------------------------------------------------------------------------- 1 | package torrent 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/anacrolix/torrent" 8 | log "github.com/sirupsen/logrus" 9 | "net/http" 10 | "net/url" 11 | "time" 12 | ) 13 | 14 | var trackers = [...]string{ 15 | "udp://open.demonii.com:1337/announce", 16 | "udp://tracker.openbittorrent.com:80", 17 | "udp://tracker.coppersurfer.tk:6969", 18 | "udp://glotorrents.pw:6969/announce", 19 | "udp://tracker.opentrackr.org:1337/announce", 20 | "udp://torrent.gresille.org:80/announce", 21 | "udp://p4p.arenabg.com:1337", 22 | "udp://tracker.leechers-paradise.org:6969", 23 | } 24 | 25 | //Client stores active Torrents and a reference to the BitTorrent client. 26 | type Client struct { 27 | Client *torrent.Client 28 | Torrents map[string]*Torrent 29 | } 30 | 31 | //Torrent stores general information about a torrent with flag indicating information and torrent availability. 32 | type Torrent struct { 33 | *torrent.Torrent 34 | Fetched bool 35 | } 36 | 37 | //Status stores Information about the download progress of a torrent. 38 | type Status struct { 39 | Name string 40 | InfoHash string 41 | BytesDownloaded int64 42 | BytesMissing int64 43 | } 44 | 45 | //NewClient creates a new BitTorrent client. 46 | func NewClient() (client *Client, err error) { 47 | var c *torrent.Client 48 | client = &Client{} 49 | client.Torrents = make(map[string]*Torrent) 50 | 51 | //config 52 | torrentCfg := torrent.NewDefaultClientConfig() 53 | torrentCfg.Seed = false 54 | torrentCfg.DataDir = "./Movies" 55 | 56 | // Create client. 57 | c, err = torrent.NewClient(torrentCfg) 58 | if err != nil { 59 | return client, fmt.Errorf("creating torrent client failed: %v", err) 60 | } 61 | client.Client = c 62 | return 63 | } 64 | 65 | //AddTorrent adds Torrent to the client. If the torrent is already added returns without error. 66 | func (c *Client) AddTorrent(infoHash string) (err error) { 67 | //if torrent already registered in client return 68 | if _, ok := c.Torrents[infoHash]; ok { 69 | return nil 70 | } 71 | 72 | t, err := c.Client.AddMagnet(BuildMagnet(infoHash, infoHash)) 73 | if err != nil { 74 | return fmt.Errorf("adding torrent failed: %v", err) 75 | } 76 | c.Torrents[infoHash] = &Torrent{Torrent: t} 77 | 78 | //wait for fetch to Download torrent 79 | go func() { 80 | <-t.GotInfo() 81 | t.DownloadAll() 82 | c.Torrents[infoHash].Fetched = true 83 | }() 84 | return 85 | } 86 | 87 | func (c *Client) getLargestFile(infoHash string) (*torrent.File, error) { 88 | var target *torrent.File 89 | var maxSize int64 90 | t, ok := c.Torrents[infoHash] 91 | if !ok { 92 | return nil, fmt.Errorf("error: unregistered infoHash") 93 | } 94 | for _, file := range t.Files() { 95 | if maxSize < file.Length() { 96 | maxSize = file.Length() 97 | target = file 98 | } 99 | } 100 | return target, nil 101 | } 102 | 103 | //MovieRequest adds a torrent identified by an info hash to the BitTorrent Client. 104 | func (c *Client) MovieRequest(w http.ResponseWriter, r *http.Request) { 105 | infoHash, err := infoHashFromRequest(r) 106 | if err != nil { 107 | log.WithField("infoHash", infoHash).Warn("MovieRequest: Request without InfoHash") 108 | w.WriteHeader(http.StatusBadRequest) 109 | return 110 | } 111 | log.WithField("infoHash", infoHash).Debug("torrent request received") 112 | if err = c.AddTorrent(infoHash); err != nil { 113 | log.WithField("infoHash", infoHash).Error("MovieRequest adding torrent: %v", err) 114 | w.WriteHeader(http.StatusBadRequest) 115 | return 116 | } 117 | 118 | //allow polling for state of torrent 119 | //torrent fetched --> HTTP 202 Accepted 120 | //torrent not yet fetched --> HTTP 200 OK => Client should continue polling state. 121 | if c.Torrents[infoHash].Fetched { 122 | w.WriteHeader(http.StatusAccepted) 123 | return 124 | } 125 | w.WriteHeader(http.StatusOK) 126 | } 127 | 128 | //MovieDelete deletes a torrent identified by an info hash from the BitTorrent Client. 129 | func (c *Client) MovieDelete(w http.ResponseWriter, r *http.Request) { 130 | infoHash, err := infoHashFromRequest(r) 131 | if err != nil { 132 | log.WithField("infoHash", infoHash).Warn("MovieDelete: Request without InfoHash") 133 | w.WriteHeader(http.StatusBadRequest) 134 | return 135 | } 136 | log.WithField("infoHash", infoHash).Debug("movie delete request received") 137 | if c.Torrents[infoHash] == nil { 138 | log.Warn("MovieDelete: tried to delete non-existent: %s", r.URL.String()) 139 | w.WriteHeader(http.StatusNotFound) 140 | return 141 | } 142 | c.Torrents[infoHash].Torrent.Drop() 143 | delete(c.Torrents, infoHash) 144 | //TODO: delete movie from fs 145 | } 146 | 147 | //TorrentStatus returns download progress information about all active torrents. 148 | func (c *Client) TorrentStatus(w http.ResponseWriter, r *http.Request) { 149 | stats := make([]Status, 0, 8) 150 | for _, t := range c.Torrents { 151 | if !t.Fetched { 152 | continue 153 | } 154 | stats = append(stats, Status{ 155 | Name: t.Name(), 156 | InfoHash: t.InfoHash().String(), 157 | BytesDownloaded: t.BytesCompleted(), 158 | BytesMissing: t.BytesMissing(), 159 | }) 160 | } 161 | err := json.NewEncoder(w).Encode(stats) 162 | if err != nil { 163 | log.Errorf("TorrentStatus encoding torrent status failed") 164 | } 165 | } 166 | 167 | // GetFile is an http handler to serve the biggest file managed by the client. 168 | func (c *Client) GetFile(w http.ResponseWriter, r *http.Request) { 169 | infoHash, err := infoHashFromRequest(r) 170 | if err != nil { 171 | log.WithField("infoHash", infoHash).Warn("GetFile: Request without InfoHash") 172 | w.WriteHeader(http.StatusBadRequest) 173 | return 174 | } 175 | log.WithField("infoHash", infoHash).Debug("movie file request received") 176 | 177 | target, err := c.getLargestFile(infoHash) 178 | if err != nil { 179 | log.WithField("infoHash", infoHash).WithError(err).Errorf("server: error getting file") 180 | w.WriteHeader(http.StatusNotFound) 181 | return 182 | } 183 | entry, err := NewFileReader(target) 184 | if err != nil { 185 | http.Error(w, err.Error(), http.StatusInternalServerError) 186 | return 187 | } 188 | 189 | defer func() { 190 | if err := entry.Close(); err != nil { 191 | log.Printf("Error closing file reader: %s\n", err) 192 | } 193 | }() 194 | http.ServeContent(w, r, target.DisplayPath(), time.Now(), entry) 195 | } 196 | 197 | func infoHashFromRequest(r *http.Request) (string, error) { 198 | packed, ok := r.URL.Query()["infoHash"] 199 | if !ok || len(packed) < 1 { 200 | return "", fmt.Errorf("infoHashFromRequest: no infoHash in Request") 201 | } 202 | return packed[0], nil 203 | } 204 | 205 | //BuildMagnet builds a magnet link from an info hash and a static list of trackers. 206 | func BuildMagnet(infoHash string, title string) string { 207 | b := &bytes.Buffer{} 208 | b.WriteString("magnet:?xt=urn:btih:") 209 | b.WriteString(infoHash) 210 | b.WriteString("&dn=") 211 | b.WriteString(url.QueryEscape(title)) 212 | for _, tracker := range trackers { 213 | b.WriteString("&tr=") 214 | b.WriteString(tracker) 215 | } 216 | return b.String() 217 | } 218 | -------------------------------------------------------------------------------- /torrent/fileEntry.go: -------------------------------------------------------------------------------- 1 | package torrent 2 | 3 | import ( 4 | "github.com/anacrolix/torrent" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // SeekableContent describes an io.ReadSeeker that can be closed as well. 10 | type SeekableContent interface { 11 | io.ReadSeeker 12 | io.Closer 13 | } 14 | 15 | // FileEntry helps reading a torrent file. 16 | type FileEntry struct { 17 | *torrent.File 18 | torrent.Reader 19 | } 20 | 21 | // Seek seeks to the correct file position, paying attention to the offset. 22 | func (f FileEntry) Seek(offset int64, whence int) (int64, error) { 23 | return f.Reader.Seek(offset+f.File.Offset(), whence) 24 | } 25 | 26 | // NewFileReader sets up a torrent file for streaming reading. 27 | func NewFileReader(f *torrent.File) (SeekableContent, error) { 28 | t := f.Torrent() 29 | reader := t.NewReader() 30 | 31 | // We read ahead 1% of the file continuously. 32 | reader.SetReadahead(f.Length() / 100) 33 | reader.SetResponsive() 34 | _, err := reader.Seek(f.Offset(), os.SEEK_SET) 35 | 36 | return &FileEntry{ 37 | File: f, 38 | Reader: reader, 39 | }, err 40 | } 41 | -------------------------------------------------------------------------------- /torrent/monitoring.go: -------------------------------------------------------------------------------- 1 | package torrent 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | "html/template" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | ) 12 | 13 | var statusTemplate *template.Template 14 | 15 | func init() { 16 | cwd, _ := os.Getwd() 17 | fp := filepath.Join(cwd, "./torrent/templates/status.html") 18 | statusTemplate = template.Must( 19 | template. 20 | New("status.html"). 21 | Funcs(template.FuncMap{"progress": downloadProgressString}). 22 | ParseFiles(fp)) 23 | } 24 | 25 | //Status serves a html with an overview of all active torrents. 26 | func (c *Client) Status(w http.ResponseWriter, r *http.Request) { 27 | if err := statusTemplate.Execute(w, c); err != nil { 28 | log.Error(fmt.Errorf("error while displaying status: %v", err)) 29 | } 30 | } 31 | 32 | func downloadProgressString(completed int64, missing int64) string { 33 | mbCompleted := strconv.FormatInt(completed/1000000, 10) 34 | mbTotal := strconv.FormatInt((completed+missing)/1000000, 10) 35 | return fmt.Sprintf("%s / %s Mb", mbCompleted, mbTotal) 36 | } 37 | -------------------------------------------------------------------------------- /torrent/templates/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | freeflix status 6 | 7 | 8 |

active torrents: {{len .Torrents}}

9 | 29 | 30 | --------------------------------------------------------------------------------