├── .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 |
5 |
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 | [](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 | 
26 |
27 | 
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 |
10 | {{range $key, $t := .Torrents}}
11 | {{if .Fetched}}
12 | -
13 | {{.String}}
14 |
15 | {{with .Stats}}
16 | - Peers: {{.ActivePeers}}
17 | - Seeders: {{.ConnectedSeeders}}
18 | {{end}}
19 | - {{progress .BytesCompleted .BytesMissing}}
20 |
21 |
22 | {{else}}
23 | -
24 | Torrent {{.String}} fetching...
25 |
26 | {{end}}
27 | {{end}}
28 |
29 |
30 |
--------------------------------------------------------------------------------