├── api ├── web │ └── .gitkeep ├── .gitignore ├── scheduler.go ├── Dockerfile ├── api.go ├── scrape.go ├── config.json ├── Makefile ├── bitcannon.go ├── import.go └── torrentdb.go ├── web ├── app │ ├── .buildignore │ ├── robots.txt │ ├── favicon.ico │ ├── images │ │ └── bitcannon.png │ ├── views │ │ ├── browse.html │ │ ├── main.html │ │ ├── about.html │ │ ├── last.html │ │ ├── search.html │ │ └── torrent.html │ ├── scripts │ │ ├── controllers │ │ │ ├── browse.js │ │ │ ├── main.js │ │ │ ├── settings.js │ │ │ ├── browsesearch.js │ │ │ ├── last.js │ │ │ ├── search.js │ │ │ └── torrent.js │ │ └── app.js │ ├── styles │ │ └── main.css │ ├── 404.html │ └── index.html ├── .gitattributes ├── .bowerrc ├── .gitignore ├── .travis.yml ├── index.js ├── Dockerfile ├── .jshintrc ├── .editorconfig ├── bower.json ├── test │ ├── spec │ │ └── controllers │ │ │ ├── main.js │ │ │ ├── browse.js │ │ │ ├── search.js │ │ │ ├── torrent.js │ │ │ ├── settings.js │ │ │ └── browsesearch.js │ ├── .jshintrc │ └── karma.conf.js ├── package.json └── Gruntfile.js ├── .gitignore ├── Makefile ├── docker-compose.yml ├── LICENSE.md └── README.md /api/web/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /web/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | BitCannon.zip 2 | data/ 3 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | web/ 3 | bindata.go 4 | -------------------------------------------------------------------------------- /web/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /web/app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | -------------------------------------------------------------------------------- /web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlyersWeb/bitcannon/HEAD/web/app/favicon.ico -------------------------------------------------------------------------------- /web/app/images/bitcannon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlyersWeb/bitcannon/HEAD/web/app/images/bitcannon.png -------------------------------------------------------------------------------- /web/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /api/scheduler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import () 4 | 5 | func runScheduler() { 6 | go importScheduler() 7 | if config.ScrapeEnabled { 8 | go scrapeWorker() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const PORT = 9000; 4 | const express = require('express'); 5 | const app = express(); 6 | 7 | app.use(express.static('dist')); 8 | 9 | app.listen(PORT, function () { 10 | console.log(`Example app listening on port ${PORT}!`); 11 | }); 12 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:5 2 | 3 | ADD . /var/www 4 | WORKDIR /var/www 5 | 6 | RUN npm install -g bower \ 7 | && npm install -g grunt-cli \ 8 | && rm -rf node_modules && rm -rf bower-components && rm -rf dist \ 9 | && npm install 10 | RUN bower install --allow-root=true --config.analytics=false --config.interactive=false 11 | RUN grunt 12 | 13 | EXPOSE 3000 14 | CMD ["npm", "start"] 15 | -------------------------------------------------------------------------------- /web/app/views/browse.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | {{category.name}} 6 |

7 |

8 | {{category.count}} torrents in this category. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/browse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:BrowseCtrl 6 | * @description 7 | * # BrowseCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('BrowseCtrl', function ($rootScope, $scope) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.7 2 | 3 | ADD . /go/src/app 4 | WORKDIR /go/src/app 5 | 6 | RUN ls -l /var \ 7 | && go get -u github.com/jteeuwen/go-bindata/... \ 8 | && go-bindata -nocompress=true web/... 9 | 10 | RUN go get -d -v \ 11 | && go install -v 12 | 13 | RUN mkdir -p build/bitcannon \ 14 | && cp config.json build/ \ 15 | && cp config.json build/bitcannon/ \ 16 | && go build -o build/bitcannon_bin 17 | 18 | CMD ["build/bitcannon_bin"] -------------------------------------------------------------------------------- /web/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /web/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bit-cannon", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "~1.3.0", 6 | "json3": "~3.3.0", 7 | "es5-shim": "~4.0.0", 8 | "angular-animate": "~1.3.0", 9 | "ui-router": "0.2.13", 10 | "bootswatch-dist": "3.3.0-flatly", 11 | "angular-loading-bar": "*", 12 | "angular-moment": "~0.9.0", 13 | "ngInfiniteScroll": "1.2.0", 14 | "fontawesome": "~4.2.0" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "~1.3.0", 18 | "angular-scenario": "~1.3.0" 19 | }, 20 | "appPath": "app" 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: build_web copy_web build_api 2 | @echo "BitCannon built to api/build" 3 | deploy: build_web copy_web deploy_api package 4 | @echo "BitCannon releases zipped in the build folder." 5 | 6 | build_web: 7 | @echo Building the web app... 8 | @cd web; \ 9 | grunt 10 | @echo Finished building web. 11 | build_api: 12 | @cd api; \ 13 | make build 14 | deploy_api: 15 | @cd api; \ 16 | make deploy 17 | copy_web: 18 | @echo Copying the web app to the api... 19 | @rm -rf api/web 20 | @cp -r web/dist api/web 21 | @touch api/web/.gitkeep 22 | package: 23 | @cd api; \ 24 | make package 25 | -------------------------------------------------------------------------------- /web/test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | MainCtrl = $controller('MainCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/test/spec/controllers/browse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: BrowseCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var BrowseCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | BrowseCtrl = $controller('BrowseCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/test/spec/controllers/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: SearchCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var SearchCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | SearchCtrl = $controller('SearchCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/test/spec/controllers/torrent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: TorrentCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var TorrentCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | TorrentCtrl = $controller('TorrentCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | bitcannon_front: 4 | build: ./web 5 | image: bitcannon_front 6 | container_name: bitcannon_front 7 | ports: 8 | - "80:9000" 9 | bitcannon_api: 10 | build: ./api 11 | image: bitcannon_api 12 | container_name: bitcannon_api 13 | links: 14 | - mongo 15 | - bitcannon_front 16 | ports: 17 | - "8000:1337" 18 | mongo: 19 | image: mongo 20 | container_name: mongo 21 | restart: always 22 | ports: 23 | - "27017:27017" 24 | volumes: 25 | - db_data:/data/db 26 | # create a named datavolume 27 | volumes: 28 | db_data: -------------------------------------------------------------------------------- /web/test/spec/controllers/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: SettingsCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var SettingsCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | SettingsCtrl = $controller('SettingsCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/test/spec/controllers/browsesearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: BrowsesearchCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('bitCannonApp')); 7 | 8 | var BrowsesearchCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | BrowsesearchCtrl = $controller('BrowsesearchCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 | 10 | 11 |
12 |
13 | {{stats.Count}} torrents in the database 14 |
15 | -------------------------------------------------------------------------------- /web/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:MainCtrl 6 | * @description 7 | * # MainCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('MainCtrl', function($scope, $state) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.submit = function() { 18 | if ($scope.query) { 19 | if ($scope.selectedCategory) { 20 | $state.go('searchCategory', { 21 | query: $scope.query, 22 | category: $scope.selectedCategory.name 23 | }); 24 | } else { 25 | $state.go('search', { 26 | query: $scope.query 27 | }); 28 | } 29 | } 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:SettingsCtrl 6 | * @description 7 | * # SettingsCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('SettingsCtrl', function ($rootScope, $scope, $window) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.saveAPI = function() { 18 | $rootScope.api = $scope.apiBox; 19 | $window.localStorage.api = $rootScope.api; 20 | }; 21 | $scope.clearAPIBox = function() { 22 | $scope.apiBox = $rootScope.api; 23 | }; 24 | $scope.resetAPI = function() { 25 | delete $window.localStorage.api; 26 | $rootScope.api = '';// Old default http://localhost:1337 27 | $scope.apiBox = $rootScope.api; 28 | }; 29 | $scope.apiBox = $rootScope.api; 30 | }); 31 | -------------------------------------------------------------------------------- /web/app/views/about.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | The BitCannon Project 4 |

5 |

6 | Visit bitcannon.io for more info 7 |

8 |

9 | The goal of BitCannon is to provide the tools to easily aggregate the content of many torrent sites into an easily browse-able format. 10 |
11 |
BitCannon aims to be as user friendly as possible while still providing robustness and the features you would expect. We hope the average user will use BitCannon to keep personal bittorrent archives, but we strive to produce code that can stand up 12 | to running a public mirror as well. 13 |

14 |

15 | How To Use 16 |

17 |

18 | Using BitCannon is simple. There is a search box on the front page that allows you to search for torrents that have been loaded into the database. You may also find torrents using the browse tab. The torrent decsription page contains torrent details and 19 | a magnet link, which will open your torrent client for downloading. 20 |
21 |

22 |
23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 BitCannon Team 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 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/browsesearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:BrowsesearchCtrl 6 | * @description 7 | * # BrowsesearchCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('BrowsesearchCtrl', function ($rootScope, $scope, $stateParams, $http) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.category = $stateParams.category; 18 | var init = function() { 19 | $http.get($rootScope.api + 'browse/' + $scope.category). 20 | success(function(data, status) { 21 | if (status === 200) { 22 | for (var i = 0; i < data.length; i++) { 23 | var row = data[i]; 24 | row.Details = '&tr='+row.Details.join('&tr='); 25 | } 26 | $scope.results = data; 27 | } 28 | else { 29 | $rootScope.message = data.message; 30 | } 31 | }). 32 | error(function() { 33 | $rootScope.message = 'API Request failed.'; 34 | }); 35 | }; 36 | init(); 37 | }); 38 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-martini/martini" 5 | "github.com/martini-contrib/cors" 6 | "github.com/martini-contrib/render" 7 | "github.com/martini-contrib/staticbin" 8 | ) 9 | 10 | type API struct { 11 | M *martini.ClassicMartini 12 | } 13 | 14 | func NewAPI() *API { 15 | m := martini.Classic() 16 | m.Use(render.Renderer()) 17 | m.Use(cors.Allow(&cors.Options{ 18 | AllowOrigins: []string{"*"}, 19 | AllowMethods: []string{"POST", "GET"}, 20 | ExposeHeaders: []string{"Content-Length"}, 21 | })) 22 | m.Use(staticbin.Static("web", Asset)) 23 | return &API{m} 24 | } 25 | 26 | func (api *API) AddRoutes() { 27 | api.M.Get("/stats", torrentDB.Stats) 28 | api.M.Get("/browse", torrentDB.Categories) 29 | api.M.Get("/browse/:category", torrentDB.Browse) 30 | api.M.Get("/torrent/:btih", torrentDB.Get) 31 | 32 | api.M.Get("/search/:query", torrentDB.Search) 33 | api.M.Get("/search/:query/s/:skip", torrentDB.Search) 34 | api.M.Get("/search/:query/c/:category", torrentDB.Search) 35 | api.M.Get("/search/:query/c/:category/s/:skip", torrentDB.Search) 36 | 37 | api.M.Get("/scrape/:btih", apiScrape) 38 | } 39 | 40 | func (api *API) Run(port string) { 41 | api.M.RunOnAddr(port) 42 | } 43 | -------------------------------------------------------------------------------- /api/scrape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Stephen304/goscrape" 5 | "github.com/go-martini/martini" 6 | "github.com/martini-contrib/render" 7 | "gopkg.in/mgo.v2/bson" 8 | "time" 9 | ) 10 | 11 | func scrapeWorker() { 12 | bulk := goscrape.NewBulk(trackers) 13 | for { 14 | stale := torrentDB.GetStale() 15 | if len(stale) > 1 { 16 | results := bulk.ScrapeBulk(stale) 17 | multiUpdate(results) 18 | } else { 19 | time.Sleep(30 * time.Second) 20 | } 21 | time.Sleep(time.Duration(config.ScrapeDelay) * time.Second) 22 | } 23 | } 24 | 25 | func multiUpdate(results []goscrape.Result) { 26 | for _, result := range results { 27 | torrentDB.Update(result.Btih, result.Seeders, result.Leechers) 28 | } 29 | } 30 | 31 | func apiScrape(r render.Render, params martini.Params) { 32 | tresult := Torrent{} 33 | err = torrentDB.collection.Find(bson.M{"_id": params["btih"]}).One(&tresult) 34 | if err != nil { 35 | r.JSON(404, map[string]interface{}{"message": "Torrent not found."}) 36 | return 37 | } 38 | result := goscrape.Single(tresult.Details, []string{params["btih"]})[0] 39 | multiUpdate([]goscrape.Result{result}) 40 | r.JSON(200, map[string]interface{}{"Swarm": map[string]interface{}{"Seeders": result.Seeders, "Leechers": result.Leechers}, "Lastmod": time.Now()}) 41 | } 42 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:BrowseCtrl 6 | * @description 7 | * # BrowseCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('LastCtrl', function ($rootScope, $scope, $http) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.busy = false; 18 | $scope.results = []; 19 | $scope.infinite = function() { 20 | if($scope.busy){return;} 21 | $scope.busy = true; 22 | $http.get($rootScope.api + 'last' + '/s/' + $scope.results.length). 23 | success(function(data, status) { 24 | if (status === 200) { 25 | for (var i = 0; i < data.length; i++) { 26 | var row = data[i]; 27 | row.Details = '&tr='+row.Details.join('&tr='); 28 | $scope.results.push(row); 29 | } 30 | if(data.length > 0) { 31 | $scope.busy = false; 32 | } 33 | } 34 | else { 35 | $rootScope.message = data.message; 36 | } 37 | }). 38 | error(function() { 39 | $rootScope.message = 'API Request failed.'; 40 | }); 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /api/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo": "mongo", 3 | "bitcannonBindIp": "0.0.0.0", 4 | "bitcannonPort": "1337", 5 | "scrapeEnabled": true, 6 | "scrapeDelay": 0, 7 | "trackers": [ 8 | "udp://tracker.coppersurfer.tk:6969/announce", 9 | "udp://tracker.leechers-paradise.org:6969/announce", 10 | "udp://tracker.zer0day.to:1337/announce", 11 | "http://tracker.opentrackr.org:1337/announce", 12 | "udp://tracker.opentrackr.org:1337/announce", 13 | "udp://p4p.arenabg.com:1337/announce", 14 | "http://p4p.arenabg.com:1337/announce", 15 | "udp://9.rarbg.com:2710/announce", 16 | "http://explodie.org:6969/announce", 17 | "udp://explodie.org:6969/announce", 18 | "udp://public.popcorn-tracker.org:6969/announce", 19 | "udp://tracker.internetwarriors.net:1337/announce", 20 | "http://tracker.dler.org:6969/announce", 21 | "http://tracker1.wasabii.com.tw:6969/announce", 22 | "http://tracker.mg64.net:6881/announce", 23 | "http://mgtracker.org:6969/announce", 24 | "udp://tracker.mg64.net:6969/announce", 25 | "udp://mgtracker.org:2710/announce", 26 | "http://tracker2.wasabii.com.tw:6969/announce", 27 | "http://tracker.tiny-vps.com:6969/announce" 28 | ], 29 | "blacklisted_categories": [ 30 | "Category to blacklist 1", 31 | "Category to blacklist 2" 32 | ], 33 | "archives": [] 34 | } 35 | -------------------------------------------------------------------------------- /web/app/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Push content to start at the bottom of the navbar */ 2 | body { 3 | padding-top: 60px; 4 | } 5 | 6 | /* Ensures that the brand button clickable area is full height */ 7 | .navbar-brand { 8 | height: 60px; 9 | } 10 | 11 | .alert { 12 | margin-top: 20px; 13 | } 14 | 15 | /* Remove dotted outline on focused elements */ 16 | a:focus, a:active, 17 | button::-moz-focus-inner, 18 | input[type="reset"]::-moz-focus-inner, 19 | input[type="button"]::-moz-focus-inner, 20 | input[type="submit"]::-moz-focus-inner, 21 | select::-moz-focus-inner, 22 | input[type="file"] > input[type="button"]::-moz-focus-inner { 23 | border: 0; 24 | outline : 0; 25 | } 26 | 27 | /* Slide down transition animations */ 28 | .animate-slide-container { 29 | position:relative; 30 | height:43px; 31 | overflow:hidden; 32 | } 33 | .animate-slide { 34 | padding:10px; 35 | } 36 | .animate-slide.ng-animate { 37 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 38 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 39 | position:absolute; 40 | top:0; 41 | left:0; 42 | right:0; 43 | bottom:0; 44 | } 45 | .animate-slide.ng-enter { 46 | top:-50px; 47 | } 48 | .animate-slide.ng-leave.ng-leave-active { 49 | top:50px; 50 | } 51 | .animate-slide.ng-leave, 52 | .animate-slide.ng-enter.ng-enter-active { 53 | top:0; 54 | } 55 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcannon", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "express": "~4.14.0" 6 | }, 7 | "devDependencies": { 8 | "grunt": "~0.4.5", 9 | "grunt-autoprefixer": "~0.7.3", 10 | "grunt-concurrent": "~0.5.0", 11 | "grunt-contrib-clean": "~0.5.0", 12 | "grunt-contrib-concat": "~0.4.0", 13 | "grunt-contrib-connect": "~0.7.1", 14 | "grunt-contrib-copy": "~0.5.0", 15 | "grunt-contrib-cssmin": "~0.9.0", 16 | "grunt-contrib-htmlmin": "~0.3.0", 17 | "grunt-contrib-imagemin": "~0.8.1", 18 | "grunt-contrib-jshint": "~0.10.0", 19 | "grunt-contrib-uglify": "~0.4.0", 20 | "grunt-contrib-watch": "~0.6.1", 21 | "grunt-filerev": "~0.2.1", 22 | "grunt-google-cdn": "~0.4.0", 23 | "grunt-karma": "~0.9.0", 24 | "grunt-newer": "~0.7.0", 25 | "grunt-ng-annotate": "~0.9.2", 26 | "grunt-svgmin": "~0.4.0", 27 | "grunt-usemin": "~2.1.1", 28 | "grunt-wiredep": "~1.7.0", 29 | "jasmine-core": "~2.1.3", 30 | "jshint-stylish": "~0.2.0", 31 | "karma": "~0.12.28", 32 | "karma-jasmine": "~0.3.2", 33 | "karma-phantomjs-launcher": "~0.1.4", 34 | "load-grunt-tasks": "~0.4.0", 35 | "time-grunt": "~0.3.1" 36 | }, 37 | "engines": { 38 | "node": ">=0.10.0" 39 | }, 40 | "scripts": { 41 | "test": "grunt test", 42 | "start": "node --harmony index.js" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/app/views/last.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | 47 |
6 | Category 7 | 9 | DL 10 | 12 | Name 13 | 15 | Imported 16 | 18 | Updated 19 | 21 | Swarm 22 |
28 | {{result.Category}} 29 | 31 | 32 | 34 | {{result.Title}} 35 | 37 | 38 | 40 | 41 | 43 | {{result.Swarm.Seeders}} {{result.Swarm.Leechers}} 44 |
48 |
49 | -------------------------------------------------------------------------------- /web/app/views/search.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | 47 |
6 | Category 7 | 9 | DL 10 | 12 | Name 13 | 15 | Imported 16 | 18 | Updated 19 | 21 | Swarm 22 |
28 | {{result.Category}} 29 | 31 | 32 | 34 | {{result.Title}} 35 | 37 | 38 | 40 | 41 | 43 | {{result.Swarm.Seeders}} {{result.Swarm.Leechers}} 44 |
48 |
49 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:SearchCtrl 6 | * @description 7 | * # SearchCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('SearchCtrl', function($rootScope, $scope, $stateParams, $http) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.query = $stateParams.query; 18 | $scope.category = $stateParams.category; 19 | $scope.results = []; 20 | if (typeof $scope.category === 'undefined') { 21 | // Do nothing 22 | } else { 23 | $scope.query = $scope.query + '/c/' + $scope.category; 24 | } 25 | $scope.busy = false; 26 | $scope.infinite = function() { 27 | if ($scope.busy) {return;} 28 | $scope.busy = true; 29 | $http.get($rootScope.api + 'search/' + $scope.query + '/s/' + $scope.results.length). 30 | success(function(data, status) { 31 | if (status === 200) { 32 | for (var i = 0; i < data.length; i++) { 33 | var row = data[i]; 34 | row.Details = '&tr='+row.Details.join('&tr='); 35 | $scope.results.push(row); 36 | } 37 | if (data.length > 0) { 38 | $scope.busy = false; 39 | } 40 | } 41 | else { 42 | $rootScope.message = data.message; 43 | } 44 | }). 45 | error(function() { 46 | $rootScope.message = 'API Request failed.'; 47 | }); 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /api/Makefile: -------------------------------------------------------------------------------- 1 | build: clean prep config 2 | go build -o build/bitcannon_bin 3 | deploy: clean prep config 4 | gox -output="build/bitcannon_{{.OS}}_{{.Arch}}" 5 | 6 | clean: 7 | @echo Cleaning the build directory... 8 | @rm -rf build 9 | prep: 10 | @echo Prepping the build directory... 11 | @mkdir -p build/bitcannon 12 | @go-bindata -nocompress=true web/... 13 | config: 14 | @echo Copying the config to build... 15 | @cp config.json build/ 16 | @cp config.json build/bitcannon/ 17 | 18 | package: 19 | @echo Packaging the Mac release 20 | @rm -rf build/bitcannon/bitcannon* 21 | @cp build/bitcannon_darwin_* build/bitcannon 22 | @cd build; \ 23 | zip -r BC_MacOSX_v$(VERSION).zip bitcannon 24 | 25 | @echo Packaging the FreeBSD release 26 | @rm -rf build/bitcannon/bitcannon* 27 | @cp build/bitcannon_freebsd_* build/bitcannon 28 | @cd build; \ 29 | zip -r BC_FreeBSD_v$(VERSION).zip bitcannon 30 | 31 | @echo Packaging the Linux release 32 | @rm -rf build/bitcannon/bitcannon* 33 | @cp build/bitcannon_linux_* build/bitcannon 34 | @cd build; \ 35 | zip -r BC_Linux_v$(VERSION).zip bitcannon 36 | 37 | @echo Packaging the NetBSD release 38 | @rm -rf build/bitcannon/bitcannon* 39 | @cp build/bitcannon_netbsd_* build/bitcannon 40 | @cd build; \ 41 | zip -r BC_NetBSD_v$(VERSION).zip bitcannon 42 | 43 | @echo Packaging the OpenBSD release 44 | @rm -rf build/bitcannon/bitcannon* 45 | @cp build/bitcannon_openbsd_* build/bitcannon 46 | @cd build; \ 47 | zip -r BC_OpenBSD_v$(VERSION).zip bitcannon 48 | 49 | @echo Packaging the Windows release 50 | @rm -rf build/bitcannon/bitcannon* 51 | @cp build/bitcannon_windows_* build/bitcannon 52 | @cd build; \ 53 | zip -r BC_Windows_v$(VERSION).zip bitcannon 54 | -------------------------------------------------------------------------------- /web/app/scripts/controllers/torrent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name bitCannonApp.controller:TorrentCtrl 6 | * @description 7 | * # TorrentCtrl 8 | * Controller of the bitCannonApp 9 | */ 10 | angular.module('bitCannonApp') 11 | .controller('TorrentCtrl', function ($rootScope, $scope, $stateParams, $http) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | $scope.btih = $stateParams.btih; 18 | $scope.showFiles = function() { 19 | if($scope.showed === false) { 20 | $scope.showed = true; 21 | } 22 | else { 23 | $scope.showed = false; 24 | } 25 | }; 26 | var init = function() { 27 | $http.get($rootScope.api + 'torrent/' + $scope.btih). 28 | success(function(data, status) { 29 | if (status === 200) { 30 | data.Url = '&tr=' + data.Details.join('&tr='); 31 | $scope.torrent = data; 32 | } 33 | else { 34 | $rootScope.message = data.message; 35 | } 36 | }). 37 | error(function() { 38 | $rootScope.message = 'API Request failed.'; 39 | }); 40 | }; 41 | $scope.refreshing = false; 42 | $scope.refresh = function() { 43 | if ($scope.refreshing) { 44 | console.log('ignored duplicate refresh request'); 45 | return; 46 | } 47 | $scope.refreshing = true; 48 | $http.get($rootScope.api + 'scrape/' + $scope.btih). 49 | success(function(data, status) { 50 | if (status === 200) { 51 | $scope.refreshing = false; 52 | $scope.torrent.Swarm = data.Swarm; 53 | $scope.torrent.Lastmod = data.Lastmod; 54 | } 55 | else { 56 | $scope.refreshing = false; 57 | $rootScope.message = data.message; 58 | } 59 | }). 60 | error(function() { 61 | $scope.refreshing = false; 62 | $rootScope.message = 'API Request failed.'; 63 | }); 64 | }; 65 | init(); 66 | }); 67 | -------------------------------------------------------------------------------- /web/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-12-22 using 4 | // generator-karma 0.8.3 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'bower_components/angular/angular.js', 22 | 'bower_components/angular-mocks/angular-mocks.js', 23 | 'bower_components/angular-animate/angular-animate.js', 24 | 'bower_components/ui-router/release/angular-ui-router.js', 25 | 'bower_components/angular-loading-bar/build/loading-bar.min.js', 26 | 'bower_components/moment/moment.js', 27 | 'bower_components/angular-moment/angular-moment.js', 28 | 'bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js', 29 | 'app/scripts/**/*.js', 30 | 'test/mock/**/*.js', 31 | 'test/spec/**/*.js' 32 | ], 33 | 34 | // list of files / patterns to exclude 35 | exclude: [], 36 | 37 | // web server port 38 | port: 8080, 39 | 40 | // Start these browsers, currently available: 41 | // - Chrome 42 | // - ChromeCanary 43 | // - Firefox 44 | // - Opera 45 | // - Safari (only Mac) 46 | // - PhantomJS 47 | // - IE (only Windows) 48 | browsers: [ 49 | 'PhantomJS' 50 | ], 51 | 52 | // Which plugins to enable 53 | plugins: [ 54 | 'karma-phantomjs-launcher', 55 | 'karma-jasmine' 56 | ], 57 | 58 | // Continuous Integration mode 59 | // if true, it capture browsers, run tests and exit 60 | singleRun: false, 61 | 62 | colors: true, 63 | 64 | // level of logging 65 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 66 | logLevel: config.LOG_INFO, 67 | 68 | // Uncomment the following lines if you are using grunt's server to run the tests 69 | // proxies: { 70 | // '/': 'http://localhost:9000/' 71 | // }, 72 | // URL root prevent conflicts with the site root 73 | // urlRoot: '_karma_' 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /web/app/views/torrent.html: -------------------------------------------------------------------------------- 1 |
2 |

{{torrent.Title}}

3 |
4 |
5 |

Torrent Details

6 |
    7 |
  • 8 | Import Date 9 | 10 |
  • 11 |
  • 12 | Category 13 | {{torrent.Category}} 14 |
  • 15 |
  • 16 | File Size 17 | {{torrent.Size}} bytes 18 |
  • 19 |
20 | Download! 21 |
22 |
23 |

Tracker Stats

24 |
    25 |
  • 26 | Seeders 27 | {{torrent.Swarm.Seeders}} 28 |
  • 29 |
  • 30 | Leechers 31 | {{torrent.Swarm.Leechers}} 32 |
  • 33 |
  • 34 | Last Updated 35 | 36 |
  • 37 |
38 | 39 | 40 | Refresh Trackers 41 | 42 |
43 |
44 |
45 |
46 |

Sources

47 | 52 |
53 |
54 |
55 |
56 |

Files

57 |
    58 |
  • 59 | {{file}} 60 |
  • 61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /api/bitcannon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "github.com/antonholmquist/jason" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | ) 10 | 11 | type Config struct { 12 | ScrapeEnabled bool 13 | ScrapeDelay int 14 | } 15 | 16 | var config = Config{ScrapeEnabled: false, ScrapeDelay: 0} 17 | var trackers []string 18 | var blacklistedCategories []string 19 | var archives []*jason.Object 20 | var torrentDB *TorrentDB 21 | var err error 22 | 23 | const resultLimit int = 200 24 | 25 | func main() { 26 | // Get mongo url from config.json, otherwise default to 127.0.0.1 27 | mongo := "127.0.0.1" 28 | bitcannonPort := "1337" 29 | bitcannonBindIp := "0.0.0.0" 30 | f, err := ioutil.ReadFile("config.json") 31 | if err != nil { 32 | log.Println("[!!!] Config not loaded") 33 | } else { 34 | json, err := jason.NewObjectFromBytes(f) 35 | if err == nil { 36 | // Get mongo connection details 37 | val, err := json.GetString("mongo") 38 | if err == nil { 39 | mongo = val 40 | } 41 | // Get desired port 42 | val, err = json.GetString("bitcannonPort") 43 | if err == nil { 44 | bitcannonPort = val 45 | } 46 | // Get archive sources 47 | arc, err := json.GetObjectArray("archives") 48 | if err == nil { 49 | archives = arc 50 | } 51 | // Get trackers 52 | trac, err := json.GetStringArray("trackers") 53 | if err == nil { 54 | trackers = trac 55 | } 56 | // Get blacklisted categories 57 | blackCats, err := json.GetStringArray("blacklisted_categories") 58 | if err == nil { 59 | blacklistedCategories = blackCats 60 | } 61 | // Get scraping enabled 62 | scrape, err := json.GetBoolean("scrapeEnabled") 63 | if err == nil { 64 | config.ScrapeEnabled = scrape 65 | } 66 | // Get scrape delay 67 | scrapeDelay, err := json.GetInt64("scrapeDelay") 68 | if err == nil { 69 | config.ScrapeDelay = int(scrapeDelay) 70 | } 71 | // Get desired listening host 72 | val, err = json.GetString("bitcannonBindIp") 73 | if err == nil { 74 | bitcannonBindIp = val 75 | } 76 | } 77 | } 78 | // Try to connect to the database 79 | log.Println("[OK!] Connecting to Mongo at " + mongo) 80 | torrentDB, err = NewTorrentDB(mongo) 81 | if err != nil { 82 | log.Println("[ERR] I'm sorry! I Couldn't connect to Mongo.") 83 | log.Println(" Please make sure it is installed and running.") 84 | return 85 | } 86 | defer torrentDB.Close() 87 | 88 | if len(os.Args) > 1 { 89 | importFile(os.Args[1]) 90 | enterExit() 91 | } else { 92 | runServer(bitcannonPort, bitcannonBindIp) 93 | } 94 | } 95 | 96 | func runServer(bitcannonPort string, bitcannonBindIp string) { 97 | log.Println("[OK!] BitCannon is live at http://localhost:" + bitcannonPort + "/") 98 | api := NewAPI() 99 | api.AddRoutes() 100 | runScheduler() 101 | api.Run(bitcannonBindIp + ":" + bitcannonPort) 102 | } 103 | 104 | func enterExit() { 105 | log.Println("\n\nPress enter to quit...") 106 | reader := bufio.NewReader(os.Stdin) 107 | reader.ReadString('\n') 108 | } 109 | -------------------------------------------------------------------------------- /web/app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name bitCannonApp 6 | * @description 7 | * # bitCannonApp 8 | * 9 | * Main module of the application. 10 | */ 11 | angular 12 | .module('bitCannonApp', [ 13 | 'ngAnimate', 14 | 'ui.router', 15 | 'angular-loading-bar', 16 | 'angularMoment', 17 | 'infinite-scroll' 18 | ]) 19 | .config(function($stateProvider, $urlRouterProvider, $compileProvider) { 20 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|magnet):/); 21 | $urlRouterProvider.otherwise('/'); 22 | $stateProvider 23 | .state('index', { 24 | url: '/', 25 | templateUrl: 'views/main.html', 26 | controller: 'MainCtrl', 27 | pageTitle: 'Home' 28 | }) 29 | .state('browse', { 30 | url: '/browse', 31 | templateUrl: 'views/browse.html', 32 | controller: 'BrowseCtrl', 33 | pageTitle: 'Browse' 34 | }) 35 | .state('last', { 36 | url: '/last', 37 | templateUrl: 'views/last.html', 38 | controller: 'LastCtrl', 39 | pageTitle: 'Latests' 40 | }) 41 | .state('browseSearch', { 42 | url: '/browse/:category', 43 | templateUrl: 'views/search.html', 44 | controller: 'BrowsesearchCtrl', 45 | pageTitle: 'Browse' 46 | }) 47 | .state('search', { 48 | url: '/search/:query', 49 | templateUrl: 'views/search.html', 50 | controller: 'SearchCtrl', 51 | pageTitle: 'Search' 52 | }) 53 | .state('searchCategory', { 54 | url: '/search/:query/c/:category', 55 | templateUrl: 'views/search.html', 56 | controller: 'SearchCtrl', 57 | pageTitle: 'Search' 58 | }) 59 | .state('torrent', { 60 | url: '/torrent/:btih', 61 | templateUrl: 'views/torrent.html', 62 | controller: 'TorrentCtrl', 63 | pageTitle: 'Torrent' 64 | }) 65 | .state('about', { 66 | url: '/about', 67 | templateUrl: 'views/about.html', 68 | controller: 'MainCtrl', 69 | pageTitle: 'About' 70 | }); 71 | }) 72 | .run(function($rootScope, $window, $http) { 73 | if (typeof $window.localStorage.api === 'undefined' || $window.localStorage.api === '') { 74 | $rootScope.api = '';// Old default http://localhost:1337 75 | } 76 | else { 77 | $rootScope.api = $window.localStorage.api; 78 | } 79 | var init = function() { 80 | $http.get($rootScope.api + 'browse'). 81 | success(function(data, status) { 82 | if (status === 200) { 83 | $rootScope.categories = data; 84 | } 85 | else { 86 | // Error! 87 | } 88 | }). 89 | error(function() { 90 | // Error! 91 | }); 92 | $http.get($rootScope.api + 'stats'). 93 | success(function(data, status) { 94 | if (status === 200) { 95 | $rootScope.stats = data; 96 | $rootScope.magnetTrackers = ''; 97 | for (var index = 0; index < data.Trackers.length; ++index) { 98 | $rootScope.magnetTrackers = $rootScope.magnetTrackers + '&tr=' + data.Trackers[index]; 99 | } 100 | $rootScope.magnetTrackers = $rootScope.magnetTrackers.replace(/\//g, '%2F'); 101 | $rootScope.magnetTrackers = $rootScope.magnetTrackers.replace(/:/g, '%3A'); 102 | } 103 | else { 104 | $rootScope.message = data.message; 105 | } 106 | }). 107 | error(function() { 108 | $rootScope.message = 'API Request failed.'; 109 | }); 110 | }; 111 | $rootScope.clearMessage = function() { 112 | $rootScope.message = null; 113 | }; 114 | init(); 115 | }); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitCannon 2 | A torrent site mirroring tool 3 | 4 | ## About 5 | The goal of BitCannon is to provide the tools to easily aggregate the content of many torrent sites and DHT network into an easily browse-able format. 6 | 7 | BitCannon aims to be as user friendly as possible while still providing robustness and the features you would expect. We hope the average user will use BitCannon to keep personal bittorrent archives, but we strive to produce code that can stand up to running a public mirror as well. 8 | 9 | ## Project Mirrors 10 | This project is based from : 11 | * [GitHub](https://github.com/Stephen304/bitcannon) 12 | * [BitBucket](https://bitbucket.org/Stephen304/bitcannon) 13 | * [Google Code](https://code.google.com/p/bitcannon/) 14 | 15 | ## How to use: Using Docker 16 | 17 | Launch the docker-compose on root directory : 18 | 19 | ``` 20 | docker-compose up -d 21 | ``` 22 | 23 | Then, you'll have the front available on localhost : 24 | 25 | ``` 26 | firefox http://localhost 27 | ``` 28 | 29 | The API will be available at http://localhost:8000 so set it in settings. 30 | 31 | You're done. 32 | 33 | ## How to use: Building From Source 34 | 35 | > If you are not a programmer or do not wish to install this long list of things, use the instructions on the wiki instead! 36 | * NodeJS 37 | * Grunt 38 | * Bower 39 | * Golang 40 | * Golang Dependencies 41 | 42 | __(Note: These building instructions may get out of date from time to time due to code changes. If you just want to use BitCannon, you should use the Wiki instructions instead.)__ 43 | 44 | ### MongoDB 45 | * Install and run MongoDB from official packages 46 | 47 | You must build the web first, as it gets embedded into the api binary. 48 | 49 | ### Web 50 | * Install node (`sudo pacman -S nodejs`) 51 | * Install bower and grunt with `sudo npm install -g grunt` and `sudo npm install -g grunt-cli` 52 | * In `/web` type `npm install`, `bower install`, and `grunt` 53 | * Check that the web built into the dist folder 54 | 55 | > If grunt fails with errors, you may have not installed it properly. The NodeJS and Grunt guys probably know more about it than I do 56 | 57 | ### API 58 | * Clone the repo 59 | * Install go (`packer -S go-git`) 60 | * Set $GOPATH (`export GOPATH=$HOME/.go`) 61 | * Set $PATH (`export PATH="$PATH:$GOPATH/bin"`) 62 | * Restart your terminal if you added these env vars to the startup script 63 | 64 | > Go can be hard to install without nice official packages. If go spits errors, try googling them a bit before opening an issue. It may not be specific to this project. 65 | 66 | * In the main folder, run `make build_api` to try to build 67 | * If go complains about dependencies, get them with `go get ` 68 | 69 | Once you have all of the dependencies, it will build into the api/build folder. 70 | 71 | * Run `bitcannon` to run the server 72 | * Run `bitcannon ` to import torrents 73 | 74 | #### Extra things and tips 75 | * If you edit the web app, typing `make build` in the main folder will recompile both the web and api into `api/build` 76 | * If you only edited the api folder, use `make build_api` to avoid recompiling the web 77 | * Optional: Cross compile for other platforms (Your go installation must be from the source or it will spit errors) 78 | * Run `go get github.com/mitchellh/gox` 79 | * Build the toolchain with `gox -build-toolchain` 80 | * Compile with `make deploy` (Will make a zip containing all the binaries) 81 | 82 | ## Progress 83 | The early version of BitCannon aims to provide import functionality from bittorrent archives and a simple interface to browse and search your torrent database. Later versions may have more advanced features like auto updating torrent indexes and possibly more. 84 | 85 | ## Works great with DHTBay 86 | 87 | Actually I've made this fork to work properly with the DHTBay project I've made, it is totally compatible, and I'll soon share with you my docker-compose file to make them work together. 88 | 89 | Meanwhile you can access the project at https://github.com/FlyersWeb/dhtbay. 90 | 91 | ## License 92 | This is MIT licensed, so do whatever you want with it. Just don't blame me for anything that happens. 93 | -------------------------------------------------------------------------------- /web/app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 |
    148 |
  • a mistyped address
  • 149 |
  • an out-of-date link
  • 150 |
151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /api/import.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "errors" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | _ "crypto/sha512" 15 | ) 16 | 17 | func importScheduler() { 18 | for _, site := range archives { 19 | url, err := site.GetString("url") 20 | if err == nil { 21 | freq, err := site.GetInt64("frequency") 22 | if err == nil { 23 | go importWorker(url, int(freq)) 24 | } 25 | importURL(url) 26 | } 27 | } 28 | log.Print("[OK!] Finished auto importing.") 29 | } 30 | 31 | func importWorker(url string, freq int) { 32 | for _ = range time.Tick(time.Duration(freq) * time.Second) { 33 | importURL(url) 34 | } 35 | } 36 | 37 | func importFile(filename string) { 38 | // Print out status 39 | log.Print("[OK!] Attempting to parse ") 40 | log.Println(filename) 41 | 42 | // Try to open the file 43 | file, err := os.Open(filename) 44 | if err != nil { 45 | log.Println("[ERR] Sorry! I Couldn't access the specified file.") 46 | log.Println(" Double check the permissions and file path.") 47 | return 48 | } 49 | defer file.Close() 50 | log.Println("[OK!] File opened") 51 | 52 | // Check file extension 53 | var gzipped bool = false 54 | if strings.HasSuffix(filename, ".txt") { 55 | gzipped = false 56 | } else if strings.HasSuffix(filename, ".csv") { 57 | gzipped = false 58 | } else if strings.HasSuffix(filename, ".txt.gz") { 59 | gzipped = true 60 | } else { 61 | log.Println("[ERR] My deepest apologies! The file doesn't meet the requirements.") 62 | log.Println(" BitCannon currently accepts .txt and gzipped .txt files only.") 63 | return 64 | } 65 | log.Println("[OK!] Extension is valid") 66 | importReader(file, gzipped) 67 | } 68 | 69 | func importURL(url string) { 70 | log.Println("[OK!] Starting to import from url:") 71 | log.Println(" " + url) 72 | response, err := http.Get(url) 73 | if err != nil { 74 | log.Println("[ERR] Oh no! Couldn't request torrent updates.") 75 | log.Println(" Is your internet working? Is BitCannon firewalled?.") 76 | return 77 | } 78 | defer response.Body.Close() 79 | 80 | var gzipped bool = false 81 | if strings.HasSuffix(url, ".txt") { 82 | gzipped = false 83 | } else if strings.HasSuffix(url, ".csv") { 84 | gzipped = false 85 | } else if strings.HasSuffix(url, ".txt.gz") { 86 | gzipped = true 87 | } else { 88 | log.Println("[!!!] I was given a URL that doesn't end in .txt or .txt.gz.") 89 | log.Println(" I'll assume it's regular text.") 90 | } 91 | log.Println("[OK!] Compression detection complete") 92 | importReader(response.Body, gzipped) 93 | } 94 | 95 | func importReader(reader io.Reader, gzipped bool) { 96 | var scanner *bufio.Scanner 97 | if gzipped { 98 | gReader, err := gzip.NewReader(reader) 99 | if err != nil { 100 | log.Println("[ERR] My bad! I tried to start uncompressing your archive but failed.") 101 | log.Println(" Try checking the file, or send me the file so I can check it out.") 102 | return 103 | } 104 | defer gReader.Close() 105 | log.Println("[OK!] GZip detected, unzipping enabled") 106 | scanner = bufio.NewScanner(gReader) 107 | } else { 108 | scanner = bufio.NewScanner(reader) 109 | } 110 | log.Println("[OK!] Reading initialized") 111 | imported := 0 112 | skipped := 0 113 | // Now we scan ୧༼ಠ益ಠ༽୨ 114 | for scanner.Scan() { 115 | status, _ := importLine(scanner.Text()) 116 | if status { 117 | imported++ 118 | } else { 119 | skipped++ 120 | } 121 | } 122 | log.Println("[OK!] Reading completed") 123 | log.Println(" " + strconv.Itoa(imported) + " torrents imported") 124 | log.Println(" " + strconv.Itoa(skipped) + " torrents skipped") 125 | } 126 | 127 | func importLine(line string) (bool, error) { 128 | var size int 129 | if strings.Count(line, "|") == 4 { 130 | data := strings.Split(line, "|") 131 | if len(data[0]) != 40 { 132 | return false, errors.New("Probably not a torrent archive") 133 | } 134 | if data[2] == "" { 135 | data[2] = "Other" 136 | } 137 | if (!categoryInBlacklisted(data[2],blacklistedCategories)) { 138 | return torrentDB.Insert(data[0], data[1], data[2], 0, data[3]) 139 | } else { 140 | return false, errors.New("Skipping torrent due to category "+data[2]+" in blacklist") 141 | } 142 | } else if strings.Count(line, "|") == 6 { 143 | data := strings.Split(line, "|") 144 | if len(data[2]) != 40 { 145 | return false, errors.New("Probably not a torrent archive") 146 | } 147 | if data[4] == "" { 148 | data[4] = "Other" 149 | } 150 | size, _ = strconv.Atoi(data[1]) 151 | if (!categoryInBlacklisted(data[4],blacklistedCategories)) { 152 | return torrentDB.Insert(data[2], data[0], data[4], size, "") 153 | } else { 154 | return false, errors.New("Skipping torrent due to category "+data[4]+" in blacklist") 155 | } 156 | } else { 157 | return false, errors.New("Something's up with this torrent.") 158 | } 159 | } 160 | 161 | //http://stackoverflow.com/questions/15323767/how-to-if-x-in-array-in-golang 162 | func categoryInBlacklisted(a string, list []string) bool { 163 | for _, b := range list { 164 | if b == a { 165 | return true 166 | } 167 | } 168 | return false 169 | } 170 | -------------------------------------------------------------------------------- /api/torrentdb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/go-martini/martini" 6 | "github.com/martini-contrib/render" 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | type TorrentDB struct { 14 | session *mgo.Session 15 | collection *mgo.Collection 16 | } 17 | 18 | type Torrent struct { 19 | Btih string `bson:"_id,omitempty"` 20 | Title string 21 | Category string 22 | Size int 23 | Details []string 24 | Swarm Stats 25 | Lastmod time.Time 26 | Imported time.Time 27 | } 28 | 29 | type Stats struct { 30 | Seeders int 31 | Leechers int 32 | } 33 | 34 | func NewTorrentDB(url string) (*TorrentDB, error) { 35 | session, err := mgo.Dial(url) 36 | if err != nil { 37 | return nil, err 38 | } 39 | session.SetMode(mgo.Monotonic, true) 40 | collection := session.DB("bitcannon").C("torrents") 41 | collection.EnsureIndex(mgo.Index{Key: []string{"$text:title"}, Name: "title"}) 42 | collection.EnsureIndex(mgo.Index{Key: []string{"category"}, Name: "category"}) 43 | collection.EnsureIndex(mgo.Index{Key: []string{"swarm.seeders"}, Name: "seeders"}) 44 | collection.EnsureIndex(mgo.Index{Key: []string{"lastmod"}, Name: "lastmod"}) 45 | return &TorrentDB{session, collection}, nil 46 | } 47 | 48 | func (torrentDB *TorrentDB) Close() { 49 | torrentDB.session.Close() 50 | } 51 | 52 | func (torrentDB *TorrentDB) Stats(r render.Render) { 53 | count, err := torrentDB.collection.Count() 54 | if err != nil { 55 | r.JSON(500, map[string]interface{}{"message": "API Error"}) 56 | return 57 | } 58 | r.JSON(200, map[string]interface{}{"Count": count, "Trackers": trackers}) 59 | } 60 | 61 | func (torrentDB *TorrentDB) Categories(r render.Render) { 62 | var result []string 63 | err := torrentDB.collection.Find(nil).Distinct("category", &result) 64 | if err != nil { 65 | r.JSON(500, map[string]interface{}{"message": "API Error"}) 66 | return 67 | } 68 | var size int 69 | for size = range result { 70 | } 71 | stats := make([]map[string]interface{}, size+1, size+1) 72 | for i, cat := range result { 73 | total, err := torrentDB.collection.Find(bson.M{"category": cat}).Count() 74 | if err != nil { 75 | stats[i] = map[string]interface{}{cat: 0} 76 | } else { 77 | stats[i] = map[string]interface{}{"name": cat, "count": total} 78 | } 79 | } 80 | r.JSON(200, stats) 81 | } 82 | 83 | func (torrentDB *TorrentDB) Browse(r render.Render, params martini.Params) { 84 | result := []Torrent{} 85 | err = torrentDB.collection.Find(bson.M{"category": params["category"]}).Sort("-swarm.seeders").Limit(resultLimit).All(&result) 86 | if err != nil { 87 | r.JSON(404, map[string]interface{}{"message": err.Error()}) 88 | return 89 | } 90 | r.JSON(200, result) 91 | } 92 | 93 | func (torrentDB *TorrentDB) Search(r render.Render, params martini.Params) { 94 | result := []Torrent{} 95 | skip := 0 96 | if value, ok := params["skip"]; ok { 97 | skip, err = strconv.Atoi(value) 98 | if err != nil { 99 | r.JSON(400, map[string]interface{}{"message": err.Error()}) 100 | return 101 | } 102 | } 103 | var pipe *mgo.Pipe 104 | if category, ok := params["category"]; ok { 105 | pipe = torrentDB.collection.Pipe([]bson.M{ 106 | {"$match": bson.M{"$text": bson.M{"$search": params["query"]}}}, 107 | {"$match": bson.M{"category": category}}, 108 | {"$sort": bson.M{"swarm.seeders": -1}}, 109 | {"$skip": skip}, 110 | {"$limit": resultLimit}, 111 | }) 112 | } else { 113 | pipe = torrentDB.collection.Pipe([]bson.M{ 114 | {"$match": bson.M{"$text": bson.M{"$search": params["query"]}}}, 115 | {"$sort": bson.M{"swarm.seeders": -1}}, 116 | {"$skip": skip}, 117 | {"$limit": resultLimit}, 118 | }) 119 | } 120 | iter := pipe.Iter() 121 | err = iter.All(&result) 122 | if err != nil { 123 | r.JSON(404, map[string]interface{}{"message": err.Error()}) 124 | return 125 | } 126 | r.JSON(200, result) 127 | } 128 | 129 | func (torrentDB *TorrentDB) Get(r render.Render, params martini.Params) { 130 | result := Torrent{} 131 | err = torrentDB.collection.Find(bson.M{"_id": params["btih"]}).One(&result) 132 | if err != nil { 133 | r.JSON(404, map[string]interface{}{"message": "Torrent not found."}) 134 | return 135 | } 136 | r.JSON(200, result) 137 | } 138 | 139 | func (torrentDB *TorrentDB) Insert(btih string, title string, category string, size int, details string) (bool, error) { 140 | var detailsArr []string 141 | if details != "" { 142 | detailsArr = []string{details} 143 | } 144 | err := torrentDB.collection.Insert( 145 | &Torrent{Btih: btih, 146 | Title: title, 147 | Category: category, 148 | Size: size, 149 | Details: detailsArr, 150 | Swarm: Stats{Seeders: -1, Leechers: -1}, 151 | Lastmod: time.Now(), 152 | Imported: time.Now(), 153 | }) 154 | if err != nil { 155 | return false, errors.New("Something went wrong when trying to insert.") 156 | } 157 | return true, nil 158 | } 159 | 160 | func (torrentDB *TorrentDB) Update(btih string, seeders int, leechers int) { 161 | match := bson.M{"_id": btih} 162 | update := bson.M{"$set": bson.M{"swarm": &Stats{Seeders: seeders, Leechers: leechers}, "lastmod": time.Now()}} 163 | torrentDB.collection.Update(match, update) 164 | } 165 | 166 | func (torrentDB *TorrentDB) GetStale() []string { 167 | result := []Torrent{} 168 | err = torrentDB.collection.Find(bson.M{"swarm.seeders": -1, "swarm.leechers": -1}).Limit(50).All(&result) 169 | if len(result) == 0 { 170 | // No unscraped torrents, get stale ones 171 | torrentDB.collection.Find(bson.M{"lastmod": bson.M{"$lt": time.Now().Add(-24 * time.Hour)}}).Sort("lastmod").Limit(50).All(&result) 172 | } 173 | var btih = make([]string, len(result)) 174 | for i := range result { 175 | btih[i] = result[i].Btih 176 | } 177 | return btih 178 | } 179 | -------------------------------------------------------------------------------- /web/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 50 | 51 |
52 | 56 |
57 | 58 |
59 | 60 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /web/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-12-22 using generator-angular 0.10.0 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Configurable paths for the application 19 | var appConfig = { 20 | app: require('./bower.json').appPath || 'app', 21 | dist: 'dist' 22 | }; 23 | 24 | // Define the configuration for all the tasks 25 | grunt.initConfig({ 26 | 27 | // Project settings 28 | yeoman: appConfig, 29 | 30 | // Watches files for changes and runs tasks based on the changed files 31 | watch: { 32 | bower: { 33 | files: ['bower.json'], 34 | tasks: ['wiredep'] 35 | }, 36 | js: { 37 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 38 | tasks: ['newer:jshint:all'], 39 | options: { 40 | livereload: '<%= connect.options.livereload %>' 41 | } 42 | }, 43 | jsTest: { 44 | files: ['test/spec/{,*/}*.js'], 45 | tasks: ['newer:jshint:test', 'karma'] 46 | }, 47 | styles: { 48 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 49 | tasks: ['newer:copy:styles', 'autoprefixer'] 50 | }, 51 | gruntfile: { 52 | files: ['Gruntfile.js'] 53 | }, 54 | livereload: { 55 | options: { 56 | livereload: '<%= connect.options.livereload %>' 57 | }, 58 | files: [ 59 | '<%= yeoman.app %>/{,*/}*.html', 60 | '.tmp/styles/{,*/}*.css', 61 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 62 | ] 63 | } 64 | }, 65 | 66 | // The actual grunt server settings 67 | connect: { 68 | options: { 69 | port: 9000, 70 | // Change this to '0.0.0.0' to access the server from outside. 71 | hostname: '0.0.0.0', 72 | livereload: 35729 73 | }, 74 | livereload: { 75 | options: { 76 | open: true, 77 | middleware: function (connect) { 78 | return [ 79 | connect.static('.tmp'), 80 | connect().use( 81 | '/bower_components', 82 | connect.static('./bower_components') 83 | ), 84 | connect.static(appConfig.app) 85 | ]; 86 | } 87 | } 88 | }, 89 | test: { 90 | options: { 91 | port: 9001, 92 | middleware: function (connect) { 93 | return [ 94 | connect.static('.tmp'), 95 | connect.static('test'), 96 | connect().use( 97 | '/bower_components', 98 | connect.static('./bower_components') 99 | ), 100 | connect.static(appConfig.app) 101 | ]; 102 | } 103 | } 104 | }, 105 | dist: { 106 | options: { 107 | open: true, 108 | base: '<%= yeoman.dist %>' 109 | } 110 | } 111 | }, 112 | 113 | // Make sure code styles are up to par and there are no obvious mistakes 114 | jshint: { 115 | options: { 116 | jshintrc: '.jshintrc', 117 | reporter: require('jshint-stylish') 118 | }, 119 | all: { 120 | src: [ 121 | 'Gruntfile.js', 122 | '<%= yeoman.app %>/scripts/{,*/}*.js' 123 | ] 124 | }, 125 | test: { 126 | options: { 127 | jshintrc: 'test/.jshintrc' 128 | }, 129 | src: ['test/spec/{,*/}*.js'] 130 | } 131 | }, 132 | 133 | // Empties folders to start fresh 134 | clean: { 135 | dist: { 136 | files: [{ 137 | dot: true, 138 | src: [ 139 | '.tmp', 140 | '<%= yeoman.dist %>/{,*/}*', 141 | '!<%= yeoman.dist %>/.git{,*/}*' 142 | ] 143 | }] 144 | }, 145 | server: '.tmp' 146 | }, 147 | 148 | // Add vendor prefixed styles 149 | autoprefixer: { 150 | options: { 151 | browsers: ['last 1 version'] 152 | }, 153 | dist: { 154 | files: [{ 155 | expand: true, 156 | cwd: '.tmp/styles/', 157 | src: '{,*/}*.css', 158 | dest: '.tmp/styles/' 159 | }] 160 | } 161 | }, 162 | 163 | // Automatically inject Bower components into the app 164 | wiredep: { 165 | app: { 166 | src: ['<%= yeoman.app %>/index.html'], 167 | ignorePath: /\.\.\// 168 | } 169 | }, 170 | 171 | // Renames files for browser caching purposes 172 | filerev: { 173 | dist: { 174 | src: [ 175 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 176 | '<%= yeoman.dist %>/styles/{,*/}*.css', 177 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 178 | '<%= yeoman.dist %>/styles/fonts/*' 179 | ] 180 | } 181 | }, 182 | 183 | // Reads HTML for usemin blocks to enable smart builds that automatically 184 | // concat, minify and revision files. Creates configurations in memory so 185 | // additional tasks can operate on them 186 | useminPrepare: { 187 | html: '<%= yeoman.app %>/index.html', 188 | options: { 189 | dest: '<%= yeoman.dist %>', 190 | flow: { 191 | html: { 192 | steps: { 193 | js: ['concat', 'uglifyjs'], 194 | css: ['cssmin'] 195 | }, 196 | post: {} 197 | } 198 | } 199 | } 200 | }, 201 | 202 | // Performs rewrites based on filerev and the useminPrepare configuration 203 | usemin: { 204 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 205 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 206 | options: { 207 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] 208 | } 209 | }, 210 | 211 | // The following *-min tasks will produce minified files in the dist folder 212 | // By default, your `index.html`'s will take care of 213 | // minification. These next options are pre-configured if you do not wish 214 | // to use the Usemin blocks. 215 | // cssmin: { 216 | // dist: { 217 | // files: { 218 | // '<%= yeoman.dist %>/styles/main.css': [ 219 | // '.tmp/styles/{,*/}*.css' 220 | // ] 221 | // } 222 | // } 223 | // }, 224 | // uglify: { 225 | // dist: { 226 | // files: { 227 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 228 | // '<%= yeoman.dist %>/scripts/scripts.js' 229 | // ] 230 | // } 231 | // } 232 | // }, 233 | // concat: { 234 | // dist: {} 235 | // }, 236 | 237 | imagemin: { 238 | dist: { 239 | files: [{ 240 | expand: true, 241 | cwd: '<%= yeoman.app %>/images', 242 | src: '{,*/}*.{png,jpg,jpeg,gif}', 243 | dest: '<%= yeoman.dist %>/images' 244 | }] 245 | } 246 | }, 247 | 248 | svgmin: { 249 | dist: { 250 | files: [{ 251 | expand: true, 252 | cwd: '<%= yeoman.app %>/images', 253 | src: '{,*/}*.svg', 254 | dest: '<%= yeoman.dist %>/images' 255 | }] 256 | } 257 | }, 258 | 259 | htmlmin: { 260 | dist: { 261 | options: { 262 | collapseWhitespace: true, 263 | conservativeCollapse: true, 264 | collapseBooleanAttributes: true, 265 | removeCommentsFromCDATA: true, 266 | removeOptionalTags: true 267 | }, 268 | files: [{ 269 | expand: true, 270 | cwd: '<%= yeoman.dist %>', 271 | src: ['*.html', 'views/{,*/}*.html'], 272 | dest: '<%= yeoman.dist %>' 273 | }] 274 | } 275 | }, 276 | 277 | // ng-annotate tries to make the code safe for minification automatically 278 | // by using the Angular long form for dependency injection. 279 | ngAnnotate: { 280 | dist: { 281 | files: [{ 282 | expand: true, 283 | cwd: '.tmp/concat/scripts', 284 | src: ['*.js', '!oldieshim.js'], 285 | dest: '.tmp/concat/scripts' 286 | }] 287 | } 288 | }, 289 | 290 | // Replace Google CDN references 291 | cdnify: { 292 | dist: { 293 | html: ['<%= yeoman.dist %>/*.html'] 294 | } 295 | }, 296 | 297 | // Copies remaining files to places other tasks can use 298 | copy: { 299 | dist: { 300 | files: [{ 301 | expand: true, 302 | dot: true, 303 | cwd: '<%= yeoman.app %>', 304 | dest: '<%= yeoman.dist %>', 305 | src: [ 306 | '*.{ico,png,txt}', 307 | '.htaccess', 308 | '*.html', 309 | 'views/{,*/}*.html', 310 | 'images/{,*/}*.{webp}', 311 | 'fonts/{,*/}*.*' 312 | ] 313 | }, { 314 | expand: true, 315 | cwd: '.tmp/images', 316 | dest: '<%= yeoman.dist %>/images', 317 | src: ['generated/*'] 318 | }, { 319 | expand: true, 320 | cwd: 'bower_components/bootstrap/dist', 321 | src: 'fonts/*', 322 | dest: '<%= yeoman.dist %>' 323 | }, { 324 | expand: true, 325 | flatten: true, 326 | src: 'bower_components/fontawesome/fonts/*', 327 | dest: 'dist/fonts' 328 | }] 329 | }, 330 | styles: { 331 | expand: true, 332 | cwd: '<%= yeoman.app %>/styles', 333 | dest: '.tmp/styles/', 334 | src: '{,*/}*.css' 335 | } 336 | }, 337 | 338 | // Run some tasks in parallel to speed up the build process 339 | concurrent: { 340 | server: [ 341 | 'copy:styles' 342 | ], 343 | test: [ 344 | 'copy:styles' 345 | ], 346 | dist: [ 347 | 'copy:styles', 348 | 'imagemin', 349 | 'svgmin' 350 | ] 351 | }, 352 | 353 | // Test settings 354 | karma: { 355 | unit: { 356 | configFile: 'test/karma.conf.js', 357 | singleRun: true 358 | } 359 | } 360 | }); 361 | 362 | 363 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 364 | if (target === 'dist') { 365 | return grunt.task.run(['build', 'connect:dist:keepalive']); 366 | } 367 | 368 | grunt.task.run([ 369 | 'clean:server', 370 | 'wiredep', 371 | 'concurrent:server', 372 | 'autoprefixer', 373 | 'connect:livereload', 374 | 'watch' 375 | ]); 376 | }); 377 | 378 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 379 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 380 | grunt.task.run(['serve:' + target]); 381 | }); 382 | 383 | grunt.registerTask('test', [ 384 | 'clean:server', 385 | 'concurrent:test', 386 | 'autoprefixer', 387 | 'connect:test', 388 | 'karma' 389 | ]); 390 | 391 | grunt.registerTask('build', [ 392 | 'clean:dist', 393 | 'wiredep', 394 | 'useminPrepare', 395 | 'concurrent:dist', 396 | 'autoprefixer', 397 | 'concat', 398 | 'ngAnnotate', 399 | 'copy:dist', 400 | 'cdnify', 401 | 'cssmin', 402 | 'uglify', 403 | 'filerev', 404 | 'usemin', 405 | 'htmlmin' 406 | ]); 407 | 408 | grunt.registerTask('default', [ 409 | 'newer:jshint', 410 | 'build' 411 | ]); 412 | }; 413 | --------------------------------------------------------------------------------