├── FUNDING.yml ├── server ├── config │ ├── allowed_api.example.js │ ├── recaptcha.example.js │ ├── google.example.js │ ├── cert_config.example.js │ ├── api_key.example.js │ ├── mongo_config.example.js │ └── mailconfig.example.js ├── VERSION.js ├── public │ ├── assets │ │ ├── images │ │ │ ├── q.png │ │ │ ├── Logo.png │ │ │ ├── s1.png │ │ │ ├── s2.png │ │ │ ├── s3.png │ │ │ ├── glight.png │ │ │ ├── gmark.png │ │ │ ├── btcdonate.png │ │ │ ├── ethdonate.png │ │ │ ├── facebook.png │ │ │ ├── favicon.ico │ │ │ ├── favicon.png │ │ │ ├── highlogo.png │ │ │ ├── loading.png │ │ │ ├── spotify.png │ │ │ ├── twitter.png │ │ │ ├── youtube.png │ │ │ ├── GitHub_Logo.png │ │ │ ├── google_play.png │ │ │ ├── squareicon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── small-square.jpg │ │ │ ├── apple-touch-icon.png │ │ │ ├── squareicon_small.png │ │ │ ├── squareicon_small_old.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-precomposed.png │ │ │ ├── browserconfig.xml │ │ │ ├── safari-pinned-tab.svg │ │ │ └── z.svg │ │ ├── manifest.json │ │ ├── admin │ │ │ └── not_authenticated │ │ │ │ └── js │ │ │ │ └── main.js │ │ ├── css │ │ │ ├── animations.css │ │ │ └── globals.css │ │ ├── js │ │ │ ├── mobileremote.js │ │ │ ├── token_apply.js │ │ │ ├── callback.js │ │ │ ├── hostcontroller.js │ │ │ └── suggestions.js │ │ ├── sclib │ │ │ └── scapi.js │ │ └── html │ │ │ └── callback.html │ ├── partials │ │ ├── remote │ │ │ ├── volume.handlebars │ │ │ ├── buttons.handlebars │ │ │ ├── input.handlebars │ │ │ └── header.handlebars │ │ ├── channel │ │ │ ├── suggestions.handlebars │ │ │ ├── chat.handlebars │ │ │ ├── tabs.handlebars │ │ │ ├── playlist.handlebars │ │ │ ├── search.handlebars │ │ │ ├── players.handlebars │ │ │ ├── client_settings.handlebars │ │ │ ├── header.handlebars │ │ │ ├── settings.handlebars │ │ │ └── modal.handlebars │ │ ├── frontpage │ │ │ ├── channels.handlebars │ │ │ ├── search.handlebars │ │ │ ├── channel.handlebars │ │ │ └── header.handlebars │ │ ├── modal │ │ │ ├── cookie.handlebars │ │ │ └── about.handlebars │ │ ├── spinner.handlebars │ │ ├── contact.handlebars │ │ ├── donate.handlebars │ │ └── footer.handlebars │ ├── layouts │ │ ├── client │ │ │ ├── channel.handlebars │ │ │ ├── remote.handlebars │ │ │ ├── frontpage.handlebars │ │ │ ├── token.handlebars │ │ │ ├── embed.handlebars │ │ │ └── main.handlebars │ │ └── admin │ │ │ ├── not_authenticated.handlebars │ │ │ └── main.handlebars │ └── service-worker.js ├── README.md ├── models │ └── user.js ├── routing │ └── client │ │ ├── icons_routing.js │ │ └── router.js ├── handlers │ ├── aggregates.js │ ├── notifications.js │ ├── db.js │ └── frontpage.js ├── apps │ ├── genre_generator.js │ ├── client.js │ └── admin.js ├── pm2.js ├── app.js ├── EVENTS.md └── REST.md ├── pm2.json ├── .gitignore ├── package.json ├── README.md └── gulpfile.js /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kasperrt] 2 | custom: ["https://www.paypal.me/zoffmusic"] 3 | -------------------------------------------------------------------------------- /server/config/allowed_api.example.js: -------------------------------------------------------------------------------- 1 | var key = [""]; 2 | 3 | module.exports = key; 4 | -------------------------------------------------------------------------------- /server/VERSION.js: -------------------------------------------------------------------------------- 1 | VERSION = 6; 2 | 3 | try { 4 | module.exports = VERSION; 5 | } catch (e) {} 6 | -------------------------------------------------------------------------------- /server/public/assets/images/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/q.png -------------------------------------------------------------------------------- /server/public/assets/images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/Logo.png -------------------------------------------------------------------------------- /server/public/assets/images/s1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/s1.png -------------------------------------------------------------------------------- /server/public/assets/images/s2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/s2.png -------------------------------------------------------------------------------- /server/public/assets/images/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/s3.png -------------------------------------------------------------------------------- /server/public/assets/images/glight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/glight.png -------------------------------------------------------------------------------- /server/public/assets/images/gmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/gmark.png -------------------------------------------------------------------------------- /server/public/assets/images/btcdonate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/btcdonate.png -------------------------------------------------------------------------------- /server/public/assets/images/ethdonate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/ethdonate.png -------------------------------------------------------------------------------- /server/public/assets/images/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/facebook.png -------------------------------------------------------------------------------- /server/public/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/favicon.ico -------------------------------------------------------------------------------- /server/public/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/favicon.png -------------------------------------------------------------------------------- /server/public/assets/images/highlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/highlogo.png -------------------------------------------------------------------------------- /server/public/assets/images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/loading.png -------------------------------------------------------------------------------- /server/public/assets/images/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/spotify.png -------------------------------------------------------------------------------- /server/public/assets/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/twitter.png -------------------------------------------------------------------------------- /server/public/assets/images/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/youtube.png -------------------------------------------------------------------------------- /server/public/assets/images/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/GitHub_Logo.png -------------------------------------------------------------------------------- /server/public/assets/images/google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/google_play.png -------------------------------------------------------------------------------- /server/public/assets/images/squareicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/squareicon.png -------------------------------------------------------------------------------- /server/config/recaptcha.example.js: -------------------------------------------------------------------------------- 1 | var recaptcha = { 2 | site: "xxxx", 3 | key: "xxxxx", 4 | } 5 | 6 | module.exports = recaptcha; 7 | -------------------------------------------------------------------------------- /server/public/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /server/public/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /server/public/assets/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/mstile-150x150.png -------------------------------------------------------------------------------- /server/public/assets/images/small-square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/small-square.jpg -------------------------------------------------------------------------------- /server/config/google.example.js: -------------------------------------------------------------------------------- 1 | var google = { 2 | "analytics": "xxxx", 3 | "adsense": "xxxx", 4 | } 5 | 6 | module.exports = google; 7 | -------------------------------------------------------------------------------- /server/public/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /server/public/assets/images/squareicon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/squareicon_small.png -------------------------------------------------------------------------------- /server/public/assets/images/squareicon_small_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/squareicon_small_old.png -------------------------------------------------------------------------------- /server/config/cert_config.example.js: -------------------------------------------------------------------------------- 1 | var cert = { 2 | privateKey: 'XX', 3 | certificate: 'XX', 4 | ca: 'XX' 5 | } 6 | 7 | module.exports = cert; 8 | -------------------------------------------------------------------------------- /server/public/assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /server/public/assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /server/public/assets/images/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoff-music/zoff/HEAD/server/public/assets/images/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /server/config/api_key.example.js: -------------------------------------------------------------------------------- 1 | var api_key = { 2 | youtube: "xxxx", 3 | soundcloud: "xx" // This can be excluded if you don't have a soundcloud key 4 | }; 5 | 6 | try { 7 | module.exports = api_key; 8 | } catch (e) {} 9 | -------------------------------------------------------------------------------- /server/public/partials/remote/volume.handlebars: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /server/config/mongo_config.example.js: -------------------------------------------------------------------------------- 1 | var mongo_config = { 2 | config: 'mydb', 3 | secret: 'secret', 4 | users: 'users', 5 | host: 'localhost', 6 | port: '27017', 7 | expire: 86400, 8 | }; 9 | 10 | module.exports = mongo_config; 11 | -------------------------------------------------------------------------------- /server/public/assets/images/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d2d2d 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "zoff", 5 | "script": "./server/pm2.js", 6 | "watch": true, 7 | "instances": "max", 8 | "exec_mode": "cluster", 9 | "ignore_watch": [ 10 | "./node_modules", 11 | "./server/public/assets/images/thumbnails" 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## Apps 2 | 3 | Under ``` /server/apps/ ```, there are two files, ``` admin.js ``` and ``` client.js ```.``` admin.js ``` are for the adminpanel, and ``` client.js ``` are for zoff itself. 4 | 5 | 6 | ## REST 7 | 8 | [Rest API info](REST.md) 9 | 10 | ## Events 11 | 12 | [Events sent form the server/to the server info](EVENTS.md) 13 | -------------------------------------------------------------------------------- /server/public/partials/channel/suggestions.handlebars: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/public/layouts/client/channel.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> channel/header}} 3 |
4 |
5 |
6 |
7 |
8 | {{#unless client}} 9 | {{> channel/players}} 10 | {{/unless}} 11 | {{> channel/tabs}} 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /server/public/partials/remote/buttons.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 3 | play_arrow 4 | 5 | 6 | pause 7 | 8 | 9 | skip_next 10 | 11 |
12 | -------------------------------------------------------------------------------- /server/config/mailconfig.example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Have a look at nodemailer's config on how to set this up https://nodemailer.com/about/ 4 | * 5 | */ 6 | 7 | var mail_config = { 8 | port: 587, 9 | host: 'smtp.example.com', 10 | auth: { 11 | user: 'ex@amp.le', 12 | pass: 'example' 13 | }, 14 | secure: true, 15 | authMethod: 'PLAIN', 16 | tls: { 17 | ciphers:'SSLv3' 18 | }, 19 | from: 'no-reply@zoff.me', 20 | to: 'contact@zoff.me' 21 | notify_mail: 'notify@mail.example', 22 | }; 23 | 24 | module.exports = mail_config; 25 | -------------------------------------------------------------------------------- /server/public/partials/remote/input.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server/public/assets/images/thumbnails/ 2 | server/config/mailconfig.js 3 | server/config/api_key.js 4 | server/config/mongo_config.js 5 | server/config/cert_config.js 6 | server/config/recaptcha.js 7 | server/config/analytics.js 8 | server/config/google.js 9 | server/config/allowed_api.js 10 | server/public/assets/dist/maps/ 11 | server/public/assets/dist/callback.min.js 12 | server/public/assets/dist/token.min.js 13 | server/public/assets/dist/embed.min.js 14 | server/public/assets/dist/main.min.js 15 | server/public/assets/dist/remote.min.js 16 | */node_modules 17 | node_modules/ 18 | scripts/ 19 | .DS_Store 20 | npm-debug.log 21 | server/npm-debug.log 22 | server/public/assets/dist/ 23 | -------------------------------------------------------------------------------- /server/public/layouts/admin/not_authenticated.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | image 4 |
5 | 6 | 7 | 8 | LOGIN 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /server/public/partials/frontpage/channels.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | 18 |
19 | 22 |
23 | -------------------------------------------------------------------------------- /server/public/layouts/client/remote.handlebars: -------------------------------------------------------------------------------- 1 | {{> remote/header}} 2 |
3 |
4 |

Remote Controller

5 |
6 |
7 | {{> remote/input}} 8 | {{> remote/buttons}} 9 | {{> remote/volume}} 10 |
11 | 12 |
13 | Here you can control another Zoff player from any device. 14 |
15 | To find the ID of your player, click the Conf menu icon on the top right of the player page, then "Remote Control". 16 |
You can either scan the QR code or type the ID manually. 17 |
18 |
19 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | // app/models/user.js 2 | // load the things we need 3 | var mongoose = require("mongoose"); 4 | var bcrypt = require("bcrypt-nodejs"); 5 | 6 | // define the schema for our user model 7 | var userSchema = mongoose.Schema({ 8 | username: String, 9 | password: String 10 | }); 11 | 12 | // methods ====================== 13 | // generating a hash 14 | userSchema.methods.generateHash = function(password) { 15 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 16 | }; 17 | 18 | // checking if password is valid 19 | userSchema.methods.validPassword = function(password) { 20 | return bcrypt.compareSync(password, this.password); 21 | }; 22 | 23 | // create the model for users and expose it to our app 24 | module.exports = mongoose.model("User", userSchema); 25 | -------------------------------------------------------------------------------- /server/public/partials/modal/cookie.handlebars: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /server/routing/client/icons_routing.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | const path = require("path"); 3 | var router = express.Router(); 4 | router.use(function(req, res, next) { 5 | next(); // make sure we go to the next routes and don't stop here 6 | }); 7 | 8 | router.route("/favicon.ico").get(function(req, res, next) { 9 | res.sendFile(path.join(pathThumbnails, "/public/assets/images/favicon.ico")); 10 | }); 11 | 12 | router.route("/browserconfig.xml").get(function(req, res, next) { 13 | res.sendFile( 14 | path.join(pathThumbnails, "/public/assets/images/browserconfig.xml") 15 | ); 16 | }); 17 | 18 | router.route("/apple-touch-icon.png").get(function(req, res, next) { 19 | res.sendFile( 20 | path.join(pathThumbnails, "/public/assets/images/apple-touch-icon.png") 21 | ); 22 | }); 23 | 24 | router.route("/apple-touch-icon-precomposed.png").get(function(req, res, next) { 25 | res.sendFile( 26 | path.join( 27 | pathThumbnails, 28 | "/public/assets/images/apple-touch-icon-precomposed.png" 29 | ) 30 | ); 31 | }); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /server/public/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Zoff", 3 | "name": "Zoff", 4 | "description": "A free YouTube based radio, where no registration is needed for listening to channels, or creating your own channels. ", 5 | "dir": "ltr", 6 | "lang": "en-US", 7 | "start_url": "/", 8 | "display": "standalone", 9 | "background_color": "#2D2D2D", 10 | "theme_color": "#2D2D2D", 11 | "orientation": "portrait", 12 | "related_applications": [ 13 | { 14 | "platform": "play", 15 | "id": "zoff.me.zoff", 16 | "url": "https://play.google.com/store/apps/details?id=zoff.me.zoff" 17 | }, 18 | { 19 | "platform": "itunes", 20 | "id": "me.zoff.zoffnative", 21 | "url": "https://itunes.apple.com/us/app/zoff/id1402037061?ls=1&mt=8" 22 | } 23 | ], 24 | "icons": [ 25 | { 26 | "src": "/assets/images/android-chrome-192x192.png", 27 | "sizes": "192x192", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/assets/images/android-chrome-512x512.png", 32 | "sizes": "512x512", 33 | "type": "image/png" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /server/public/assets/admin/not_authenticated/js/main.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", function() { 2 | document 3 | .getElementById("login_button") 4 | .addEventListener("click", function(event) { 5 | event.preventDefault(); 6 | document.querySelector("#login_form").submit(); 7 | }); 8 | 9 | document 10 | .getElementById("login_form") 11 | .addEventListener("submit", function(event) { 12 | if (this.password.value == "" || this.username.value == "") { 13 | e.preventDefault(); 14 | } 15 | }); 16 | 17 | if ( 18 | window.location.pathname == "/signup/" || 19 | window.location.pathname == "/signup" 20 | ) { 21 | document 22 | .querySelector("#login_form") 23 | .insertAdjacentHTML( 24 | "afterbegin", 25 | "" 26 | ); 27 | document.querySelector("#login_form").setAttribute("action", "/signup"); 28 | } 29 | if (window.location.hash == "#failed") { 30 | window.location.hash = ""; 31 | M.toast({ 32 | html: "Couldn't find a user with that username or password..", 33 | displayLength: 4000, 34 | classes: "red lighten" 35 | }); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /server/public/partials/modal/about.handlebars: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /server/public/layouts/client/frontpage.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{> frontpage/header}} 3 | {{> frontpage/search}} 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Most Popular 15 | 16 |
17 | 22 |
23 |
24 |
25 | {{> frontpage/channels}} 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /server/public/partials/spinner.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /server/public/partials/frontpage/search.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Create a radio channel and collaborate
4 |
5 |
6 | zoff.me/ 7 | 21 |
22 | autorenew 23 |
24 | 25 | 26 |
27 |
28 |
29 |
Live, free & democratic playlists with YouTube and SoundCloud
30 |
Play everywhere — No login required
31 |
32 |
33 | -------------------------------------------------------------------------------- /server/handlers/aggregates.js: -------------------------------------------------------------------------------- 1 | var toShowConfig = { 2 | addsongs: true, 3 | adminpass: 1, 4 | allvideos: 1, 5 | frontpage: 1, 6 | longsongs: 1, 7 | removeplay: 1, 8 | shuffle: 1, 9 | skip: 1, 10 | startTime: 1, 11 | userpass: 1, 12 | vote: 1, 13 | toggleChat: { $ifNull: ["$toggleChat", true] }, 14 | strictSkip: { $ifNull: ["$strictSkip", false] }, 15 | strictSkipNumber: { $ifNull: ["$strictSkipNumber", 10] }, 16 | description: { $ifNull: ["$description", ""] }, 17 | thumbnail: { $ifNull: ["$thumbnail", ""] }, 18 | rules: { $ifNull: ["$rules", ""] }, 19 | _id: 0 20 | }; 21 | 22 | var project_object = { 23 | _id: 0, 24 | id: 1, 25 | added: 1, 26 | now_playing: 1, 27 | title: 1, 28 | votes: 1, 29 | start: 1, 30 | duration: 1, 31 | end: 1, 32 | type: 1, 33 | added_by: { $ifNull: ["$added_by", "Anonymous"] }, 34 | source: { $ifNull: ["$source", "youtube"] }, 35 | thumbnail: { 36 | $ifNull: [ 37 | "$thumbnail", 38 | { 39 | $concat: ["https://img.youtube.com/vi/", "$id", "/mqdefault.jpg"] 40 | } 41 | ] 42 | }, 43 | tags: { $ifNull: ["$tags", []] } 44 | }; 45 | 46 | var toShowChannel = { 47 | start: 1, 48 | end: 1, 49 | added: 1, 50 | id: 1, 51 | title: 1, 52 | votes: 1, 53 | duration: 1, 54 | type: 1, 55 | _id: 0, 56 | tags: 1, 57 | now_playing: 1, 58 | type: 1, 59 | source: 1, 60 | thumbnail: 1 61 | }; 62 | 63 | module.exports.project_object = project_object; 64 | module.exports.toShowConfig = toShowConfig; 65 | module.exports.toShowChannel = toShowChannel; 66 | -------------------------------------------------------------------------------- /server/handlers/notifications.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | function requested_change(type, string, channel) { 4 | try { 5 | //channel = channel.replace(/ /g,''); 6 | var nodemailer = require("nodemailer"); 7 | var mailconfig = require(path.join(__dirname, "../config/mailconfig.js")); 8 | 9 | let transporter = nodemailer.createTransport(mailconfig); 10 | 11 | transporter.verify(function(error, success) { 12 | if (error) { 13 | return; 14 | } else { 15 | var message = 16 | "A " + 17 | type + 18 | " change was requested on " + 19 | channel + 20 | "

New supposed value is:

" + 21 | string + 22 | "


\ 23 | Go to https://admin.zoff.me/ to accept or decline the request."; 24 | var msg = { 25 | from: mailconfig.from, 26 | to: mailconfig.notify_mail, 27 | subject: "ZOFF: Requested new " + type, 28 | text: message, 29 | html: message 30 | }; 31 | transporter.sendMail(msg, (error, info) => { 32 | if (error) { 33 | transporter.close(); 34 | return; 35 | } 36 | transporter.close(); 37 | }); 38 | } 39 | }); 40 | } catch (e) { 41 | console.log( 42 | "(!) Missing file - /config/mailconfig.js Have a look at /config/mailconfig.example.js. " 43 | ); 44 | } 45 | } 46 | 47 | module.exports.requested_change = requested_change; 48 | -------------------------------------------------------------------------------- /server/public/assets/css/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes snow { 2 | 0% { 3 | background-position: 0px 0px, 0px 0px, 0px 0px; 4 | } 5 | 100% { 6 | background-position: 500px 500px, 400px 400px, 300px 300px; 7 | } 8 | } 9 | @-moz-keyframes snow { 10 | 0% { 11 | background-position: 0px 0px, 0px 0px, 0px 0px; 12 | } 13 | 100% { 14 | background-position: 500px 500px, 400px 400px, 300px 300px; 15 | } 16 | } 17 | @-webkit-keyframes snow { 18 | 0% { 19 | background-position: 0px 0px, 0px 0px, 0px 0px; 20 | } 21 | 100% { 22 | background-position: 500px 500px, 400px 400px, 300px 300px; 23 | } 24 | } 25 | @-ms-keyframes snow { 26 | 0% { 27 | background-position: 0px 0px, 0px 0px, 0px 0px; 28 | } 29 | 100% { 30 | background-position: 500px 500px, 400px 400px, 300px 300px; 31 | } 32 | } 33 | 34 | /* 35 | * 36 | * 37 | * Source: https://codepen.io/NickyCDK/pen/AIonk 38 | * 39 | */ 40 | 41 | #snow { 42 | pointer-events: none; 43 | background: none; 44 | font-family: Androgyne; 45 | background-image: url("/assets/images/s1.png"), url("/assets/images/s2.png"), 46 | url("/assets/images/s3.png"); 47 | height: 100%; 48 | left: 0; 49 | position: absolute; 50 | top: 0; 51 | width: 100%; 52 | z-index: 1; 53 | -webkit-animation: snow 10s linear infinite; 54 | -moz-animation: snow 10s linear infinite; 55 | -ms-animation: snow 10s linear infinite; 56 | animation: snow 10s linear infinite; 57 | } 58 | 59 | #snow.snow-channel { 60 | z-index: 9999; 61 | width: calc(100% - 0.75rem); 62 | height: calc(100% - 32px); 63 | } 64 | -------------------------------------------------------------------------------- /server/public/assets/css/globals.css: -------------------------------------------------------------------------------- 1 | .material-icons { 2 | display: none; 3 | width: 24px; 4 | } 5 | 6 | html { 7 | background: #2d2d2d; 8 | } 9 | 10 | body { 11 | background: white; 12 | } 13 | 14 | a { 15 | outline: 0 !important; 16 | } 17 | 18 | .selectable { 19 | user-select: text; 20 | cursor: text; 21 | } 22 | 23 | body { 24 | display: flex; 25 | min-height: 100vh; 26 | flex-direction: column; 27 | overflow-x: hidden; 28 | -webkit-transition: background-color 1s; 29 | -moz-transition: background-color 1s; 30 | -ms-transition: background-color 1s; 31 | -o-transition: background-color 1s; 32 | transition: background-color 1s; 33 | overflow-y: scroll !important; 34 | } 35 | 36 | input[type="text"]:focus:not([readonly]), 37 | input[type="password"]:focus:not([readonly]), 38 | input[type="email"]:focus:not([readonly]), 39 | input[type="url"]:focus:not([readonly]), 40 | input[type="time"]:focus:not([readonly]), 41 | input[type="date"]:focus:not([readonly]), 42 | input[type="datetime-local"]:focus:not([readonly]), 43 | input[type="tel"]:focus:not([readonly]), 44 | input[type="number"]:focus:not([readonly]), 45 | input[type="search"]:focus:not([readonly]), 46 | textarea.materialize-textarea:focus:not([readonly]) { 47 | border-bottom: 1px solid #9d9d9d; 48 | box-shadow: 0 1px 0 0 #9d9d9d; 49 | } 50 | 51 | nav ul li:hover, 52 | nav ul li.active { 53 | background-color: rgba(0, 0, 0, 0.1); 54 | } 55 | 56 | .full-height { 57 | height: 100vh; 58 | } 59 | 60 | * { 61 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 62 | -moz-tap-highlight-color: rgba(0, 0, 0, 0); 63 | } 64 | -------------------------------------------------------------------------------- /server/public/partials/contact.handlebars: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /server/public/partials/channel/chat.handlebars: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /server/public/partials/channel/tabs.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{#unless client}} 3 | 20 | {{/unless}} 21 |
22 |
23 | 25 |
26 | 0/0 27 |
28 | filter_list 29 | clear 30 |
31 |
32 | {{> channel/playlist}} 33 | {{#unless client}} 34 | {{> channel/suggestions}} 35 | {{> channel/chat}} 36 | {{/unless}} 37 |
-------------------------------------------------------------------------------- /server/public/partials/frontpage/channel.handlebars: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 |
    5 |
    6 | 7 | star_rate 9 | 10 |

    11 | {{decodeString _id}} 12 |
    13 | Viewers:  14 | {{viewers}} 15 |
    16 | Length:  17 | {{count}} 18 |
    19 |

    20 | Playing:  21 | {{title}} 22 |
    23 |

    24 |
    25 |
    26 | Listen 27 |
    28 | {{#if_equal description "This list has no description"}} 29 | {{else}} 30 |
    31 | {{decodeString _id}} 32 |

    {{description}}

    33 |
    34 | {{/if_equal}} 35 |
    36 |
    37 |
  • -------------------------------------------------------------------------------- /server/public/partials/remote/header.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 18 | {{> modal/about}} 19 | 30 |
    31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zoff", 3 | "version": "2.0.2", 4 | "description": "Zoff, the shared YouTube based radio services", 5 | "main": "server/app.js", 6 | "scripts": { 7 | "start": "npm install --only=dev && npm install && $(npm bin)/gulp build && node server/app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/zoff-music/zoff.git" 13 | }, 14 | "author": { 15 | "name": "Kasper Rynning Tønnesen", 16 | "email": "kasper@kasperrt.no" 17 | }, 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/zoff-music/zoff/issues" 21 | }, 22 | "devDependencies": { 23 | "gulp": "^4.0.0", 24 | "gulp-concat": "^2.6.1", 25 | "gulp-uglify": "^3.0.2" 26 | }, 27 | "homepage": "https://github.com/zoff-music/zoff#readme", 28 | "dependencies": { 29 | "bad-words": "^1.6.5", 30 | "bcrypt-nodejs": "0.0.3", 31 | "body-parser": "^1.18.3", 32 | "color-thief-jimp": "^2.0.2", 33 | "compression": "^1.7.3", 34 | "connect-mongo": "^3.2.0", 35 | "cookie-parser": "^1.4.4", 36 | "cors": "^2.8.5", 37 | "express": "^4.17.1", 38 | "express-handlebars": "^3.0.2", 39 | "express-recaptcha": "^3.0.1", 40 | "express-session": "^1.15.6", 41 | "farmhash": "^3.0.0", 42 | "feature-policy": "^0.2.0", 43 | "gulp-clean-css": "^4.2.0", 44 | "gulp-sourcemaps": "^2.6.5", 45 | "gulp-uglify-es": "^1.0.4", 46 | "helmet": "^3.21.1", 47 | "jimp": "^0.2.28", 48 | "mongodb": "^3.4.0", 49 | "mongojs": "^2.6.0", 50 | "mongojs-paginate": "^1.2.0", 51 | "mongoose": "^5.7.5", 52 | "mpromise": "^0.5.5", 53 | "nodemailer": "^4.7.0", 54 | "passport": "^0.4.0", 55 | "passport-local": "^1.0.0", 56 | "redis": "^2.8.0", 57 | "referrer-policy": "^1.1.0", 58 | "request": "^2.88.0", 59 | "socket.io": "^2.2.0", 60 | "socket.io-redis": "^5.2.0", 61 | "sticky-session": "^1.1.2", 62 | "uniqid": "5.0.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/public/partials/donate.handlebars: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /server/public/service-worker.js: -------------------------------------------------------------------------------- 1 | var version = 'v5.0'; 2 | var CACHE_FILES = [ 3 | '/assets/html/offline.html', 4 | '/assets/manifest.json', 5 | '/assets/images/favicon.ico' 6 | ]; 7 | 8 | self.addEventListener("install", function(event) { 9 | event.waitUntil( 10 | caches 11 | .open(version + '::zoff') 12 | .then(function(cache) { 13 | return cache.addAll(CACHE_FILES); 14 | }) 15 | .then(function() { 16 | }) 17 | ); 18 | }); 19 | 20 | self.addEventListener("activate", function(event) { 21 | 22 | var cacheWhitelist = version; 23 | 24 | event.waitUntil( 25 | caches.keys().then(function(keyList) { 26 | return Promise.all(keyList.map(function(key) { 27 | if (!key.startsWith(cacheWhitelist)) { 28 | return caches.delete(key); 29 | } 30 | })); 31 | }) 32 | ); 33 | }); 34 | 35 | self.addEventListener('fetch', event => { 36 | if (event.request.mode === 'navigate' || 37 | (event.request.method === 'GET' && 38 | (event.request.headers.get('accept').includes('text/html') || 39 | event.request.headers.get('accept').includes('text/css') || 40 | (event.request.headers.get('accept').includes('*/*') && 41 | (event.request.url.includes('localhost') || event.request.url.includes('zoff.me')))))) { 42 | event.respondWith( 43 | fetch(event.request).catch(error => { 44 | if(event.request.url.includes('manifest.json')){ 45 | return caches.open(version + "::zoff").then(function(cache) { 46 | return cache.match("/assets/manifest.json"); 47 | }); 48 | } else if (event.request.url.includes('favicon')) { 49 | return caches.open(version + "::zoff").then(function(cache) { 50 | return cache.match("/assets/images/favicon.ico"); 51 | }); 52 | } else if (event.request.url.includes('service-worker')) { 53 | return caches.open(version + "::zoff").then(function(cache) { 54 | return cache.match("/service-worker.js"); 55 | }); 56 | } else { 57 | return caches.open(version + "::zoff").then(function(cache) { 58 | return cache.match("/assets/html/offline.html"); 59 | }); 60 | } 61 | }) 62 | ); 63 | } 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /server/public/layouts/admin/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zoff Admin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 61 | 62 | 63 | {{{body}}} 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /server/handlers/db.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | try { 3 | var mongo_config = require(path.join( 4 | path.join(__dirname, "../config/"), 5 | "mongo_config.js" 6 | )); 7 | } catch (e) { 8 | console.log( 9 | "(!) Missing file - /config/mongo_config.js. Have a look at /config/mongo_config.example.js. The server won't run without this existing." 10 | ); 11 | process.exit(1); 12 | } 13 | var mongojs = require("mongojs"); 14 | var db = mongojs("mongodb://" + mongo_config.host + "/" + mongo_config.config); 15 | var connected_db = mongojs( 16 | "mongodb://" + mongo_config.host + "/user_credentials" 17 | ); 18 | var ObjectId = mongojs.ObjectId; 19 | 20 | db.collection("chat_logs").createIndex( 21 | { createdAt: 1 }, 22 | { expireAfterSeconds: 600 }, 23 | function() {} 24 | ); 25 | db.collection("timeout_api").createIndex( 26 | { createdAt: 1 }, 27 | { expireAfterSeconds: 120 }, 28 | function() {} 29 | ); 30 | db.collection("api_links").createIndex( 31 | { createdAt: 1 }, 32 | { expireAfterSeconds: 86400 }, 33 | function() {} 34 | ); 35 | db.on("connected", function(err) { 36 | console.log("connected"); 37 | }); 38 | 39 | db.on("error", function(err) { 40 | console.log("\n" + new Date().toString() + "\n Database error: ", err); 41 | }); 42 | 43 | db.on("error", function(err) { 44 | console.log("\n" + new Date().toString() + "\n Database error: ", err); 45 | }); 46 | 47 | /* Resetting usernames, and connected users */ 48 | db.collection("unique_ids").update( 49 | { _id: "unique_ids" }, 50 | { $set: { unique_ids: [] } }, 51 | { multi: true, upsert: true }, 52 | function(err, docs) {} 53 | ); 54 | db.collection("user_names").remove( 55 | { guid: { $exists: true } }, 56 | { multi: true, upsert: true }, 57 | function(err, docs) {} 58 | ); 59 | db.collection("user_names").update( 60 | { _id: "all_names" }, 61 | { $set: { names: [] } }, 62 | { multi: true, upsert: true }, 63 | function(err, docs) {} 64 | ); 65 | db.collection("connected_users").update( 66 | { users: { $exists: true } }, 67 | { $set: { users: [] } }, 68 | { multi: true, upsert: true }, 69 | function(err, docs) {} 70 | ); 71 | db.collection("connected_users").update( 72 | { _id: "total_users" }, 73 | { $set: { total_users: [] } }, 74 | { multi: true, upsert: true }, 75 | function(err, docs) {} 76 | ); 77 | db.collection("frontpage_lists").update( 78 | { viewers: { $ne: 0 } }, 79 | { $set: { viewers: 0 } }, 80 | { multi: true, upsert: true }, 81 | function(err, docs) {} 82 | ); 83 | 84 | module.exports = db; 85 | -------------------------------------------------------------------------------- /server/public/partials/channel/playlist.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 27 |
    28 |
    29 | 30 | first_page 31 | 32 | 33 | first_page 34 | 35 | 36 | navigate_before prev 37 | 38 | 39 | navigate_before prev 40 | 41 | 1 42 | 43 | next navigate_next 44 | 45 | 46 | next navigate_next 47 | 48 | 49 | last_page 50 | 51 | 52 | last_page 53 | 54 |
    55 | -------------------------------------------------------------------------------- /server/apps/genre_generator.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var publicPath = path.join(__dirname, 'public'); 3 | var pathThumbnail = __dirname; 4 | pathThumbnails = __dirname + "/../"; 5 | var time_regex = /P((([0-9]*\.?[0-9]*)Y)?(([0-9]*\.?[0-9]*)M)?(([0-9]*\.?[0-9]*)W)?(([0-9]*\.?[0-9]*)D)?)?(T(([0-9]*\.?[0-9]*)H)?(([0-9]*\.?[0-9]*)M)?(([0-9]*\.?[0-9]*)S)?)?/; 6 | try { 7 | var keys = require(path.join(__dirname, '../config/api_key.js')); 8 | var key = keys.youtube; 9 | var soundcloudKey = keys.soundcloud; 10 | } catch(e) { 11 | console.log("Error - missing file"); 12 | console.log("Seems you forgot to create the file api_key.js in /server/config/. Have a look at api_key.example.js."); 13 | process.exit(1); 14 | } 15 | var Search = require(pathThumbnail + '/../handlers/search.js'); 16 | var request = require('request'); 17 | var db = require(pathThumbnail + '/../handlers/db.js'); 18 | var currentList = 0; 19 | var listNames = []; 20 | db.getCollectionNames(function(e, d) { 21 | for(var i = 0; i < d.length; i++) { 22 | if(d[i].indexOf("_") < 0) { 23 | if(d[i].length > 0) { 24 | if(d[i].substring(0, 1) == "." || d[i].substring(d[i].length - 1) == ".") continue; 25 | } 26 | listNames.push(d[i]); 27 | } 28 | } 29 | console.log("Number of lists is " + listNames.length); 30 | /*for(var i = 0; i < listNames.length; i++) { 31 | getListItems(d[i]); 32 | if(i > 1000) return; 33 | }*/ 34 | recursivifyListLooping(listNames, 0); 35 | }); 36 | 37 | function filterFunction(el) { 38 | return el != null && 39 | el != "" && 40 | el != undefined && 41 | el.trim() != '' 42 | } 43 | 44 | function recursivifyListLooping(listNames, i) { 45 | if(i > listNames.length) { 46 | console.log("Done"); 47 | return; 48 | } 49 | console.log("List " + i + " of " + listNames.length); 50 | getListItems(listNames, 0, function() { 51 | console.log("done"); 52 | }); 53 | } 54 | 55 | function getListItems(arr, i, callback) { 56 | console.log("List " + i + " of " + listNames.length + " - " + arr[i]); 57 | if(i >= arr.length) { 58 | if(typeof(callback) == "function") callback(); 59 | return; 60 | } 61 | try { 62 | db.collection(arr[i]).find(function(e, d) { 63 | if(d.length > 0) { 64 | Search.get_genres_list_recursive(d, arr[i], function(){ 65 | getListItems(arr, i + 1, callback); 66 | }); 67 | } else { 68 | getListItems(arr, i + 1, callback); 69 | } 70 | }); 71 | } catch(e) { 72 | getListItems(arr, i + 1, callback); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/pm2.js: -------------------------------------------------------------------------------- 1 | var cluster = require("cluster"), 2 | net = require("net"), 3 | path = require("path"), 4 | //publicPath = path.join(__dirname, 'public'), 5 | http = require("http"), 6 | port = 8080, 7 | //farmhash = require('farmhash'), 8 | uniqid = require("uniqid"), 9 | num_processes = require("os").cpus().length; 10 | 11 | publicPath = path.join(__dirname, "public"); 12 | pathThumbnails = __dirname; 13 | 14 | var redis = require("redis"); 15 | var client = redis.createClient({ host: "localhost", port: 6379 }); 16 | 17 | startSingle(true, true); 18 | 19 | function startSingle(clustered, redis_enabled) { 20 | var server; 21 | var client = require("./apps/client.js"); 22 | try { 23 | var cert_config = require(path.join( 24 | path.join(__dirname, "config"), 25 | "cert_config.js" 26 | )); 27 | var fs = require("fs"); 28 | var privateKey = fs.readFileSync(cert_config.privateKey).toString(); 29 | var certificate = fs.readFileSync(cert_config.certificate).toString(); 30 | var ca = fs.readFileSync(cert_config.ca).toString(); 31 | var credentials = { 32 | key: privateKey, 33 | cert: certificate, 34 | ca: ca 35 | }; 36 | var https = require("https"); 37 | server = https.Server(credentials, routingFunction); 38 | } catch (err) { 39 | console.log("Starting without https (probably on localhost)"); 40 | server = http.createServer(routingFunction); 41 | } 42 | 43 | server.listen(port, onListen); 44 | 45 | var socketIO = client.socketIO; 46 | 47 | var redis = require("socket.io-redis"); 48 | try { 49 | socketIO.adapter(redis({ host: "localhost", port: 6379 })); 50 | } catch (e) { 51 | console.log("No redis-server to connect to.."); 52 | } 53 | socketIO.listen(server); 54 | } 55 | 56 | function onListen() { 57 | console.log("Started with pid [" + process.pid + "]"); 58 | } 59 | 60 | function routingFunction(req, res, next) { 61 | var client = require("./apps/client.js"); 62 | var admin = require("./apps/admin.js"); 63 | try { 64 | var url = req.headers["x-forwarded-host"] 65 | ? req.headers["x-forwarded-host"] 66 | : req.headers.host.split(":")[0]; 67 | var subdomain = req.headers["x-forwarded-host"] 68 | ? req.headers["x-forwarded-host"].split(".") 69 | : req.headers.host.split(":")[0].split("."); 70 | 71 | if (subdomain.length > 1 && subdomain[0] == "admin") { 72 | admin(req, res, next); 73 | } else { 74 | client(req, res, next); 75 | } 76 | } catch (e) { 77 | console.log("Bad request for " + req.headers.host + req.url, e); 78 | res.statusCode = 500; 79 | res.write("Bad request"); //write a response to the client 80 | res.end(); //end the response 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/public/assets/js/mobileremote.js: -------------------------------------------------------------------------------- 1 | var Mobile_remote = { 2 | id: "", 3 | 4 | get_input: function(value) { 5 | if (Mobile_remote.id === "") { 6 | Mobile_remote.set_id(value.toLowerCase()); 7 | } else { 8 | Mobile_remote.set_channel(value.toLowerCase()); 9 | } 10 | }, 11 | 12 | set_id: function(id) { 13 | Mobile_remote.id = id; 14 | document.getElementById("pausebutton_remote").removeAttribute("disabled"); 15 | document 16 | .getElementById("skipbutton_remote") 17 | .removeAttribute("disabled", false); 18 | document 19 | .getElementById("playbutton_remote") 20 | .removeAttribute("disabled", false); 21 | document 22 | .getElementById("skipbutton_remote") 23 | .removeAttribute("disabled", false); 24 | document.getElementById("remote_channel").value = ""; 25 | document 26 | .getElementById("remote_channel") 27 | .setAttribute("placeholder", "Change channel"); 28 | document.getElementById("remote_header").innerText = "Controlling " + id; 29 | Helper.css("#volume-control-remote", "display", "inline-block"); 30 | document 31 | .querySelector(".slider-vol-mobile") 32 | .setAttribute("style", "display: inline-block !important"); 33 | }, 34 | 35 | set_channel: function(channel_name) { 36 | socket.emit("id", { 37 | id: Mobile_remote.id, 38 | type: "channel", 39 | value: channel_name 40 | }); 41 | }, 42 | 43 | play_remote: function() { 44 | socket.emit("id", { id: Mobile_remote.id, type: "play", value: "mock" }); 45 | }, 46 | 47 | pause_remote: function() { 48 | socket.emit("id", { id: Mobile_remote.id, type: "pause", value: "mock" }); 49 | }, 50 | 51 | skip_remote: function() { 52 | socket.emit("id", { id: Mobile_remote.id, type: "skip", value: "mock" }); 53 | }, 54 | 55 | initiate_volume: function() { 56 | var vol = 100; 57 | document 58 | .getElementById("volume-control-remote") 59 | .insertAdjacentHTML( 60 | "beforeend", 61 | "
    " 62 | ); 63 | document 64 | .getElementById("volume-control-remote") 65 | .insertAdjacentHTML( 66 | "beforeend", 67 | "
    " 68 | ); 69 | Helper.css(".volume-slid-remote", "width", vol + "%"); 70 | Helper.css(".volume-handle-remote", "left", "calc(" + vol + "% - 1px)"); 71 | document.getElementById("volume-control-remote").addEventListener( 72 | "touchstart", 73 | function(e) { 74 | e.preventDefault(); 75 | Playercontrols.dragMouseDown(e); 76 | }, 77 | false 78 | ); 79 | 80 | document.getElementById("volume-control-remote").addEventListener( 81 | "touchmove", 82 | function(e) { 83 | e.preventDefault(); 84 | Playercontrols.elementDrag(e); 85 | }, 86 | false 87 | ); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /server/public/partials/channel/search.handlebars: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/public/assets/js/token_apply.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", function(e) { 2 | M.Modal.init(document.getElementById("about")); 3 | M.Modal.init(document.getElementById("contact")); 4 | Helper.addClass(".help-button-footer", "hide"); 5 | 6 | Helper.setHtml("#contact-container", ""); 7 | Helper.setHtml( 8 | "#contact-container", 9 | "Send a mail to us: contact@zoff.me" 10 | ); 11 | Helper.css("#submit-contact-form", "display", "none"); 12 | 13 | var page = window.location.pathname; 14 | if (page.substring(page.length - 1) != "/") page += "/"; 15 | ga("send", "pageview", page); 16 | 17 | if (!Helper.mobilecheck()) { 18 | if (document.querySelector("#iframe-container")) { 19 | document 20 | .getElementById("iframe-container") 21 | .insertAdjacentHTML( 22 | "beforeend", 23 | '' 24 | ); 25 | } 26 | } 27 | 28 | document 29 | .getElementsByClassName("token-form")[0] 30 | .addEventListener("submit", function(e) { 31 | e.preventDefault(); 32 | var email = document.getElementById("email_address").value; 33 | var origin = document.getElementById("origin").value; 34 | document.getElementById("origin").setAttribute("readonly", true); 35 | document.getElementById("email_address").setAttribute("readonly", true); 36 | Helper.toggleClass(".submit", "disabled"); 37 | Helper.removeClass(".full-form-token", "hide"); 38 | var captcha_response = grecaptcha.getResponse(); 39 | Helper.ajax({ 40 | type: "POST", 41 | url: "/api/apply", 42 | headers: { "Content-Type": "application/json;charset=UTF-8" }, 43 | data: { 44 | origin: origin, 45 | email: email, 46 | "g-recaptcha-response": captcha_response 47 | }, 48 | success: function(response) { 49 | Helper.addClass(".full-form-token", "hide"); 50 | if (response == "success") { 51 | M.toast({ 52 | html: "Email sent!", 53 | displayLength: 3000, 54 | classes: "green lighten" 55 | }); 56 | } else { 57 | document 58 | .getElementById("email_address") 59 | .setAttribute("readonly", false); 60 | Helper.toggleClass(".submit", "disabled"); 61 | document.getElementById("origin").setAttribute("readonly", false); 62 | grecaptcha.reset(); 63 | M.toast({ 64 | html: 65 | "Something went wrong. Sure that email hasn't been used for another token?", 66 | displayLength: 3000, 67 | classes: "red lighten" 68 | }); 69 | } 70 | }, 71 | error: function(response) { 72 | Helper.addClass(".full-form-token", "hide"); 73 | document 74 | .getElementById("email_address") 75 | .setAttribute("readonly", false); 76 | Helper.toggleClass(".submit", "disabled"); 77 | } 78 | }); 79 | }); 80 | 81 | document 82 | .getElementById("submit-contact-form") 83 | .addEventListener("click", function(e) { 84 | e.preventDefault(); 85 | document.getElementById("contact-form").submit(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /server/public/partials/frontpage/header.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 23 | {{> modal/about}} 24 | 38 | 50 | 51 |
    52 | -------------------------------------------------------------------------------- /server/public/assets/images/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 21 | 22 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Zoff 2 | ==== 3 | 4 | Zoff (pronounced __søff__) is a shared (free) YouTube and SoundCloud based radio service, built upon the YouTube API, and SoundCloud API, with integrated casting with Chromecast. 5 | 6 | Zoff supports importing YouTube, SoundCloud and Spotify playlists, and has functionality that (tries to) export to YouTube, SoundCloud and Spotify. 7 | 8 | Get it on Google Play 9 | Get it on the AppStore 10 | 11 | 12 | 13 | 14 | ## Install 15 | 16 | Prerequisites: 17 | 18 | ``` 19 | MongoDB : https://www.mongodb.org/ 20 | NodeJS : https://nodejs.org/en/ 21 | npm : https://www.npmjs.com/ 22 | ``` 23 | 24 | Clone this repository into a folder, and navigate to it. Use ```$ npm install``` in the project folder. 25 | 26 | For the server to run, you have to have the files 27 | 28 | ``` 29 | api_key.js 30 | mongo_config.js 31 | ``` 32 | 33 | in ```/server/config```. There are ```*.example.js``` files for all the ones mentioned above. If you're going to deploy the server with a certificate, you also need to create the ```cert_config.js``` in ```/server/config/```. If you want the mailing to work, take a look at ```mailconfig.example.js``` and ```recaptcha.example.js```. You'll need ```mailconfig.js``` and ```recaptcha.js``` for this to work. 34 | 35 | If you want to use Google Analytics, have a look at ```analytics.example.js``` in ```server/config/```. 36 | 37 | Use ```$ npm start``` to start the server. (Alternative you can use the ```pm2.json``` in the project-root, if you prefer pm2 for running the apps.) 38 | 39 | More info in server/ README 40 | 41 | ### About 42 | 43 | Zoff is mainly a webbased service. The website uses NodeJS with Socket.IO, MongoDB and express on the backend, with JavaScript and Materialize on the frontend. 44 | 45 | The team consists of Kasper Rynning-Tønnesen and Nicolas Almagro Tonne, and the project has been worked on since late 2014. 46 | 47 | ### Contact 48 | 49 | The team can be reached on contact@zoff.me 50 | 51 | ### Screenshots of desktop version: 52 | 53 | ![Frontpage desktop](https://puu.sh/xCI8P/bbfbdd694c.png) 54 | 55 | ![Channel desktop](https://puu.sh/EI9Dt/05dea0ae57.png) 56 | 57 | ![Channel settings](https://puu.sh/EI9DV/0df8e9a5b2.png) 58 | 59 | ![Channel join](https://puu.sh/EI9E8/6f3810fe7f.png) 60 | 61 | ![Channel search desktop](https://puu.sh/EI9EJ/459deda44d.png) 62 | 63 | ![Channel host mode desktop](https://puu.sh/EI9Fb/6c1776230f.png) 64 | 65 | ### Embedded player: 66 | 67 | ![embedded](https://puu.sh/EI9HY/54434384af.png) 68 | 69 | ### Screenshots of the mobile version: 70 | 71 | ![mobilefront](http://i.imgur.com/aWlEmIx.png) 72 | ![mobile1](https://puu.sh/EI9Iz/8673bb3065.png) 73 | ![mobile2](https://puu.sh/EI9IS/5d6c3e303a.png) 74 | 75 | ### Legal 76 | 77 | Creative Commons License 78 | Zoff is licensed under a 79 | Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Norway License.. 80 | Do not redistribute without permission from the developers. 81 | 82 | Copyright © 2019 83 | Kasper Rynning-Tønnesen and Nicolas Almagro Tonne 84 | -------------------------------------------------------------------------------- /server/public/layouts/client/token.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 14 | {{> modal/about}} 15 |
    16 |
    17 |
    18 |
    19 | {{#if activated}} 20 |

    API-token

    21 |

    Here is your api token

    22 |

    {{token}}

    23 |

    Use it wisely, and don't lose it!

    24 |

    As of now, the tokens have a limit of 20 requests a second. If you need a higher limit, just contact the team and we'll set you up for as much as you need.

    25 | {{else}} 26 |

    API-token

    27 |

    Apply for a API-token with your email here! You'll get an email on the specified address, with a link. Follow that link, and the token will be shown to you! Take good care of it, and don't lose it. It won't be shown to you again.

    28 |

    If you're wondering anything about how the api works, there is a guide on our GitHub. You can also click HERE to be taken to the detailed README.

    29 |

    As of now, the tokens have a limit of 20 requests a second. If you need a higher limit, just contact the team and we'll set you up for as much as you need.

    30 |

    If you want to restrict the token for use on one domain only, you can change the Origin from * to the website of your choice (if you want for https://zoff.me, then you'd input zoff.me).

    31 | {{/if}} 32 |
    33 | {{#if activated}} 34 |
    35 | {{else}} 36 |
    37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 | 44 | 45 |
    46 |
    47 |
    48 | {{{captcha}}} 49 |
    50 |
    51 | 52 |
    53 |
    54 |
    55 | {{> spinner}} 56 |
    57 |
    58 |
    59 | {{/if}} 60 |

    Any lost tokens can easily be deleted by our admins, so just send us an email if something goes awry. Just click the CONTACT button in the footer, and we will be with you as fast as we can!

    61 |
    62 |
    63 |
    64 | -------------------------------------------------------------------------------- /server/public/assets/js/callback.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | var query = getQueryHash(window.location.hash); 3 | var redirect = 4 | window.location.protocol + "//" + window.location.hostname + "/api/oauth"; 5 | var client_id; 6 | var response; 7 | var scope; 8 | 9 | if (query.spotify) { 10 | client_id = "b934ecdd173648f5bcd38738af529d58"; 11 | response = "token"; 12 | scope = 13 | "playlist-read-private ugc-image-upload playlist-read-collaborative user-read-private playlist-modify-public playlist-modify-private"; 14 | state = query.nonce; 15 | window.location.href = 16 | "https://accounts.spotify.com/authorize?client_id=" + 17 | client_id + 18 | "&scope=" + 19 | scope + 20 | "&show_dialog=false&response_type=" + 21 | response + 22 | "&redirect_uri=" + 23 | redirect + 24 | "&state=" + 25 | state; 26 | } else if (query.youtube) { 27 | client_id = 28 | "944988770273-butsmlr1aotlsskk8lmgvh0etqqekigf.apps.googleusercontent.com"; 29 | response = "token"; 30 | scope = "https://www.googleapis.com/auth/youtube"; 31 | state = query.nonce; 32 | 33 | //window.opener.callback(query); 34 | window.location.href = 35 | "https://accounts.google.com/o/oauth2/v2/auth?client_id=" + 36 | client_id + 37 | "&response_type=" + 38 | response + 39 | "&state=" + 40 | state + 41 | "&redirect_uri=" + 42 | redirect + 43 | "&scope=" + 44 | scope; 45 | } else if (query.soundcloud) { 46 | /* 47 | SC.initialize({ 48 | client_id: api_key.soundcloud, 49 | redirect_uri: 'https://zoff.me/api/oauth' 50 | }); 51 | 52 | // initiate auth popup 53 | console.log("asd ok", api_key.soundcloud); 54 | SC.connect().then(function() { 55 | return SC.get('/me'); 56 | }).then(function(me) { 57 | console.log(me); 58 | //alert('Hello, ' + me.username); 59 | }).catch(function(e) { 60 | console.log(e); 61 | });*/ 62 | 63 | var redirect_uri = encodeURIComponent("https://zoff.me/api/oauth"); 64 | var response_type = "code"; 65 | var scope = "non-expiring"; 66 | var state = query.nonce; 67 | var url = 68 | "https://soundcloud.com/connect?client_id=" + 69 | api_key.soundcloud + 70 | "&redirect_uri=" + 71 | redirect_uri + 72 | "&state=" + 73 | state + 74 | "&display=page&response_type=code&scope=" + 75 | scope; 76 | //console.log(url); 77 | window.location.href = url; 78 | } else { 79 | var query_parameters; 80 | if (window.location.search.length > 0) { 81 | query_parameters = getQueryHash(window.location.search); 82 | } else { 83 | query_parameters = getQueryHash(window.location.hash); 84 | } 85 | try { 86 | window.opener.callback(query_parameters); 87 | } catch (e) { 88 | window.setTimeout(window.opener.SC_player.connectCallback, 1); 89 | } 90 | } 91 | }); 92 | 93 | function getQueryHash(url) { 94 | if (window.location.search.length > 0) { 95 | if (url.substring(url.length - 1) == "#") { 96 | url = url.substring(0, url.length - 1); 97 | } 98 | var temp_arr = url.substring(1).split("&"); 99 | var done_obj = {}; 100 | var splitted; 101 | for (var i in temp_arr) { 102 | splitted = temp_arr[i].split("="); 103 | if (splitted.length == 2) { 104 | done_obj[splitted[0]] = splitted[1]; 105 | } 106 | } 107 | return done_obj; 108 | } else { 109 | var temp_arr = url.substring(1).split("&"); 110 | var done_obj = {}; 111 | var splitted; 112 | for (var i in temp_arr) { 113 | splitted = temp_arr[i].split("="); 114 | if (splitted.length == 2) { 115 | done_obj[splitted[0]] = splitted[1]; 116 | } 117 | } 118 | return done_obj; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /server/public/assets/js/hostcontroller.js: -------------------------------------------------------------------------------- 1 | var Hostcontroller = { 2 | enabled: true, 3 | 4 | old_id: null, 5 | 6 | host_listener: function(id) { 7 | if (client) return; 8 | Helper.log(["Host-listener triggered", "Host-listener id:" + id]); 9 | if (Hostcontroller.old_id === null) Hostcontroller.old_id = id; 10 | else { 11 | socket.removeAllListeners(id); 12 | began = false; 13 | Hostcontroller.old_id = id; 14 | } 15 | var codeURL = 16 | window.location.protocol + "//remote." + window.location.host + "/" + id; 17 | if (embed) { 18 | if (window.parentWindow && window.parentOrigin) { 19 | window.parentWindow.postMessage( 20 | { type: "controller", id: id }, 21 | window.parentOrigin 22 | ); 23 | } 24 | } else if (!embed) { 25 | if (window.location.pathname == "/") return; 26 | document.querySelector("#code-text").innerText = id; 27 | document 28 | .querySelector("#code-qr") 29 | .setAttribute( 30 | "src", 31 | "https://chart.googleapis.com/chart?chs=221x221&cht=qr&choe=UTF-8&chld=L|1&chl=" + 32 | codeURL 33 | ); 34 | document.querySelector("#code-link").setAttribute("href", codeURL); 35 | } 36 | if (!began) { 37 | began = true; 38 | setup_host_listener(id); 39 | } 40 | }, 41 | 42 | host_on_action: function(arr) { 43 | if (client) return; 44 | if (Hostcontroller.enabled) { 45 | if (arr.type == "volume") { 46 | try { 47 | Playercontrols.visualVolume(arr.value); 48 | Player.setVolume(arr.value); 49 | if (scUsingWidget) Player.soundcloud_player.setVolume(arr.value); 50 | else Player.soundcloud_player.setVolume(arr.value / 100); 51 | try { 52 | localStorage.setItem("volume", arr.value); 53 | } catch (e) {} 54 | Playercontrols.choose_button(arr.value, false); 55 | } catch (e) {} 56 | } else if (arr.type == "channel") { 57 | if (window.location.pathname == "/") return; 58 | socket.emit("change_channel"); 59 | Admin.beginning = true; 60 | 61 | chan = arr.value.toLowerCase(); 62 | Helper.setHtml("#chan", Helper.upperFirst(chan)); 63 | var shareCodeUrl = 64 | window.location.protocol + 65 | "//client." + 66 | window.location.hostname + 67 | "/r/" + 68 | btoa(encodeURIComponent(chan.toLowerCase())); 69 | document 70 | .getElementById("share-join-qr") 71 | .setAttribute( 72 | "src", 73 | "https://chart.googleapis.com/chart?chs=221x221&cht=qr&choe=UTF-8&chld=L|1&chl=" + 74 | shareCodeUrl 75 | ); 76 | Helper.setHtml( 77 | "#channel-name-join", 78 | "client." + 79 | window.location.hostname + 80 | "/" + 81 | encodeURIComponent(chan.toLowerCase()) 82 | ); 83 | w_p = true; 84 | var add = ""; 85 | //if(private_channel) add = Crypt.getCookie("_uI") + "_"; 86 | socket.emit("list", { 87 | version: parseInt(_VERSION), 88 | channel: add + chan.toLowerCase() 89 | }); 90 | 91 | window.history.pushState( 92 | "object or string", 93 | "Title", 94 | "/" + chan.toLowerCase() 95 | ); 96 | } else if (arr.type == "pause") { 97 | Player.pauseVideo(); 98 | } else if (arr.type == "play") { 99 | Player.playVideo(); 100 | } else if (arr.type == "skip") { 101 | List.skip(); 102 | } 103 | } 104 | }, 105 | 106 | change_enabled: function(val) { 107 | if (client) return; 108 | Hostcontroller.enabled = val; 109 | try { 110 | document.querySelector(".remote_switch_class").checked = 111 | Hostcontroller.enabled; 112 | } catch (e) {} 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /server/public/partials/channel/players.handlebars: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{#unless embed}} 4 |
    5 | {{/unless}} 6 |
    7 |
    8 |
    9 |
    10 |
    11 | cast 12 |
    13 |
    14 |
    15 |
    16 | 17 | 19 | 20 | Artist 21 |
    22 |
    23 | Waiting for Video 24 |
    25 |
    26 |
    27 | {{> spinner}} 28 |
    29 |
    30 |
    31 |
    32 | play_arrow 33 | pause 34 |
    35 |
    36 | volume_off 37 | volume_mute 38 | volume_down 39 | volume_up 40 |
    41 |
    42 |
    43 |
    44 |
    45 | 48 |
    49 | play_arrow 50 | pause 51 |
    52 | 55 |
    56 | fullscreen 57 |
    58 |
    59 | volume_off 60 | volume_mute 61 | volume_down 62 | volume_up 63 |
    64 |
    65 |
    66 |
    67 |
    68 | playlist_add 69 |
    70 |
    71 |
    72 |
    73 | Add to other list 74 |
    75 | 76 |
    77 |
    78 |
    79 |
    80 |
    -------------------------------------------------------------------------------- /server/public/partials/channel/client_settings.handlebars: -------------------------------------------------------------------------------- 1 |
  • 2 |
    Client Settings 3 | settings 4 |
    5 |
    6 | 92 |
    93 |
  • 94 | -------------------------------------------------------------------------------- /server/handlers/frontpage.js: -------------------------------------------------------------------------------- 1 | var Functions = require(pathThumbnails + "/handlers/functions.js"); 2 | var db = require(pathThumbnails + "/handlers/db.js"); 3 | function frontpage_lists(msg, socket) { 4 | if ( 5 | msg == undefined || 6 | !msg.hasOwnProperty("version") || 7 | msg.version != VERSION || 8 | msg.version == undefined 9 | ) { 10 | var result = { 11 | version: { 12 | expected: VERSION, 13 | got: msg.hasOwnProperty("version") ? msg.version : undefined 14 | } 15 | }; 16 | socket.emit("update_required", result); 17 | return; 18 | } 19 | 20 | db.collection("frontpage_lists").find({ frontpage: true }, function( 21 | err, 22 | docs 23 | ) { 24 | db.collection("connected_users").find({ _id: "total_users" }, function( 25 | err, 26 | tot 27 | ) { 28 | socket 29 | .compress(true) 30 | .emit("playlists", { 31 | channels: docs, 32 | viewers: tot[0].total_users.length 33 | }); 34 | }); 35 | }); 36 | } 37 | 38 | function get_frontpage_lists(callback) { 39 | var project_object = { 40 | _id: 1, 41 | count: 1, 42 | frontpage: 1, 43 | id: 1, 44 | title: 1, 45 | viewers: 1, 46 | accessed: 1, 47 | pinned: { $ifNull: ["$pinned", 0] }, 48 | description: { 49 | $ifNull: [ 50 | { 51 | $cond: { 52 | if: { 53 | $or: [ 54 | { $eq: ["$description", ""] }, 55 | { $eq: ["$description", null] }, 56 | { $eq: ["$description", undefined] } 57 | ] 58 | }, 59 | then: "This list has no description", 60 | else: "$description" 61 | } 62 | }, 63 | "This list has no description" 64 | ] 65 | }, 66 | thumbnail: { 67 | $ifNull: [ 68 | { 69 | $cond: { 70 | if: { 71 | $or: [ 72 | { $eq: ["$thumbnail", ""] }, 73 | { $eq: ["$thumbnail", null] }, 74 | { $eq: ["$thumbnail", undefined] } 75 | ] 76 | }, 77 | then: { 78 | $concat: ["https://img.youtube.com/vi/", "$id", "/mqdefault.jpg"] 79 | }, 80 | else: "$thumbnail" 81 | } 82 | }, 83 | { $concat: ["https://img.youtube.com/vi/", "$id", "/mqdefault.jpg"] } 84 | ] 85 | } 86 | }; 87 | db.collection("frontpage_lists").aggregate( 88 | [ 89 | { 90 | $match: { 91 | frontpage: true, 92 | count: { $gt: 3 } 93 | } 94 | }, 95 | { 96 | $project: project_object 97 | }, 98 | { 99 | $sort: { 100 | pinned: -1, 101 | viewers: -1, 102 | accessed: -1, 103 | count: -1, 104 | title: 1 105 | } 106 | } 107 | ], 108 | callback 109 | ); 110 | } 111 | 112 | function update_frontpage(coll, id, title, thumbnail, source, callback) { 113 | //coll = coll.replace(/ /g,''); 114 | db.collection("frontpage_lists").find({ _id: coll }, function(e, doc) { 115 | var updateObject = { 116 | id: id, 117 | title: title, 118 | accessed: Functions.get_time() 119 | }; 120 | if ( 121 | doc.length > 0 && 122 | ((doc[0].thumbnail != "" && 123 | doc[0].thumbnail != undefined && 124 | (doc[0].thumbnail.indexOf("https://i1.sndcdn.com") > -1 || 125 | doc[0].thumbnail.indexOf("https://w1.sndcdn.com") > -1 || 126 | doc[0].thumbnail.indexOf("https://img.youtube.com") > -1)) || 127 | (doc[0].thumbnail == "" || doc[0].thumbnail == undefined)) 128 | ) { 129 | updateObject.thumbnail = thumbnail; 130 | if (thumbnail == undefined) updateObject.thumbnail = ""; 131 | } 132 | db.collection("frontpage_lists").update( 133 | { _id: coll }, 134 | { $set: updateObject }, 135 | { upsert: true }, 136 | function(err, returnDocs) { 137 | if (typeof callback == "function") callback(); 138 | } 139 | ); 140 | }); 141 | } 142 | 143 | module.exports.get_frontpage_lists = get_frontpage_lists; 144 | module.exports.frontpage_lists = frontpage_lists; 145 | module.exports.update_frontpage = update_frontpage; 146 | -------------------------------------------------------------------------------- /server/public/assets/sclib/scapi.js: -------------------------------------------------------------------------------- 1 | var SC=SC||{};SC.Widget=function(n){function t(r){if(e[r])return e[r].exports;var o=e[r]={exports:{},id:r,loaded:!1};return n[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=n,t.c=e,t.p="",t(0)}([function(n,t,e){function r(n){return!!(""===n||n&&n.charCodeAt&&n.substr)}function o(n){return!!(n&&n.constructor&&n.call&&n.apply)}function i(n){return!(!n||1!==n.nodeType||"IFRAME"!==n.nodeName.toUpperCase())}function a(n){var t,e=!1;for(t in b)if(b.hasOwnProperty(t)&&b[t]===n){e=!0;break}return e}function s(n){var t,e,r;for(t=0,e=I.length;t-1,a=new _(n),I.push(new y(a,n,o)),a)},O.Events=L,window.SC=window.SC||{},window.SC.Widget=O,y=function(n,t,e){this.instance=n,this.element=t,this.domain=u(t.getAttribute("src")),this.isReady=!!e,this.callbacks={}},_=function(){},_.prototype={constructor:_,load:function(n,t){if(n){t=t||{};var e=this,r=p(this),o=r.element,i=o.src,a=i.substr(0,i.indexOf("?"));r.isReady=!1,r.playEventFired=!1,o.onload=function(){e.bind(L.READY,function(){var n,e=r.callbacks;for(n in e)e.hasOwnProperty(n)&&n!==L.READY&&f(N.ADD_LISTENER,n,r.element);t.callback&&t.callback()})},o.src=R(a,n,t)}},bind:function(n,t){var e=this,r=p(this);return r&&r.element&&(n===L.READY&&r.isReady?setTimeout(t,1):r.isReady?(d(n,t,r),f(N.ADD_LISTENER,n,r.element)):d(C,function(){e.bind(n,t)},r)),this},unbind:function(n){var t,e=p(this);e&&e.element&&(t=E(n,e),n!==L.READY&&t&&f(N.REMOVE_LISTENER,n,e.element))}},S(_.prototype,l(b)),S(_.prototype,l(P),!0)},function(n,t){t.api={LOAD_PROGRESS:"loadProgress",PLAY_PROGRESS:"playProgress",PLAY:"play",PAUSE:"pause",FINISH:"finish",SEEK:"seek",READY:"ready",OPEN_SHARE_PANEL:"sharePanelOpened",CLICK_DOWNLOAD:"downloadClicked",CLICK_BUY:"buyClicked",ERROR:"error"},t.bridge={REMOVE_LISTENER:"removeEventListener",ADD_LISTENER:"addEventListener"}},function(n,t){n.exports={GET_VOLUME:"getVolume",GET_DURATION:"getDuration",GET_POSITION:"getPosition",GET_SOUNDS:"getSounds",GET_CURRENT_SOUND:"getCurrentSound",GET_CURRENT_SOUND_INDEX:"getCurrentSoundIndex",IS_PAUSED:"isPaused"}},function(n,t){n.exports={PLAY:"play",PAUSE:"pause",TOGGLE:"toggle",SEEK_TO:"seekTo",SET_VOLUME:"setVolume",NEXT:"next",PREV:"prev",SKIP:"skip"}}]); 2 | -------------------------------------------------------------------------------- /server/public/partials/channel/header.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 89 | {{> channel/modal}} 90 |
    -------------------------------------------------------------------------------- /server/public/partials/footer.handlebars: -------------------------------------------------------------------------------- 1 | 75 | -------------------------------------------------------------------------------- /server/public/assets/images/z.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 45 | 92 | 93 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var cluster = require("cluster"), 2 | net = require("net"), 3 | path = require("path"), 4 | //publicPath = path.join(__dirname, 'public'), 5 | http = require("http"), 6 | port = 8080, 7 | farmhash = require("farmhash"), 8 | uniqid = require("uniqid"), 9 | num_processes = require("os").cpus().length; 10 | 11 | publicPath = path.join(__dirname, "public"); 12 | pathThumbnails = __dirname; 13 | 14 | try { 15 | var redis = require("redis"); 16 | var client = redis.createClient({ host: "localhost", port: 6379 }); 17 | client.on("error", function(err) { 18 | console.log("Couldn't connect to redis-server, assuming non-clustered run"); 19 | num_processes = 1; 20 | startSingle(false, false); 21 | client.quit(); 22 | }); 23 | client.on("connect", function() { 24 | startClustered(true); 25 | client.quit(); 26 | }); 27 | } catch (e) { 28 | console.log("Couldn't connect to redis-server, assuming non-clustered run"); 29 | num_processes = 1; 30 | startSingle(false, false); 31 | } 32 | 33 | function startClustered(redis_enabled) { 34 | //Found https://stackoverflow.com/questions/40885592/use-node-js-cluster-with-socket-io-chat-application 35 | if (cluster.isMaster) { 36 | var workers = []; 37 | var spawn = function(i) { 38 | workers[i] = cluster.fork(); 39 | workers[i].on("exit", function(code, signal) { 40 | if (code == 1) { 41 | process.exit(1); 42 | return; 43 | } 44 | console.log("respawning worker", i); 45 | spawn(i); 46 | }); 47 | }; 48 | 49 | for (var i = 0; i < num_processes; i++) { 50 | spawn(i); 51 | } 52 | 53 | var worker_index = function(ip, len) { 54 | //console.log(ip); 55 | var s = ""; 56 | if (ip !== undefined) { 57 | return farmhash.fingerprint32(ip) % len; 58 | } 59 | ip = uniqid.time(); 60 | for (var i = 0, _len = ip.length; i < _len; i++) { 61 | if (!isNaN(ip[i])) { 62 | s += ip[i]; 63 | } 64 | } 65 | return Number(s) % len; 66 | }; 67 | 68 | var server = net 69 | .createServer({ pauseOnConnect: true }, function(connection, a) { 70 | var worker = 71 | workers[worker_index(connection.address().address, num_processes)]; 72 | worker.send("sticky-session:connection", connection); 73 | }) 74 | .listen(port); 75 | } else { 76 | startSingle(true, redis_enabled); 77 | } 78 | } 79 | 80 | function startSingle(clustered, redis_enabled) { 81 | var server; 82 | var client = require("./apps/client.js"); 83 | try { 84 | var cert_config = require(path.join( 85 | path.join(__dirname, "config"), 86 | "cert_config.js" 87 | )); 88 | var fs = require("fs"); 89 | var privateKey = fs.readFileSync(cert_config.privateKey).toString(); 90 | var certificate = fs.readFileSync(cert_config.certificate).toString(); 91 | var ca = fs.readFileSync(cert_config.ca).toString(); 92 | var credentials = { 93 | key: privateKey, 94 | cert: certificate, 95 | ca: ca 96 | }; 97 | var https = require("https"); 98 | server = https.Server(credentials, routingFunction); 99 | } catch (err) { 100 | console.log("Starting without https (probably on localhost)"); 101 | server = http.createServer(routingFunction); 102 | } 103 | 104 | if (clustered) { 105 | server.listen(onListen); 106 | } else { 107 | server.listen(port, onListen); 108 | } 109 | 110 | var socketIO = client.socketIO; 111 | 112 | if (redis_enabled) { 113 | var redis = require("socket.io-redis"); 114 | try { 115 | socketIO.adapter(redis({ host: "localhost", port: 6379 })); 116 | } catch (e) { 117 | console.log("No redis-server to connect to.."); 118 | } 119 | } 120 | socketIO.listen(server, { 121 | cookie: false 122 | }); 123 | 124 | process.on("message", function(message, connection) { 125 | if (message !== "sticky-session:connection") { 126 | return; 127 | } 128 | server.emit("connection", connection); 129 | connection.resume(); 130 | }); 131 | } 132 | 133 | function onListen() { 134 | console.log("Started with pid [" + process.pid + "]"); 135 | } 136 | 137 | function routingFunction(req, res, next) { 138 | var client = require("./apps/client.js"); 139 | var admin = require("./apps/admin.js"); 140 | try { 141 | var url = req.headers["x-forwarded-host"] 142 | ? req.headers["x-forwarded-host"] 143 | : req.headers.host.split(":")[0]; 144 | var subdomain = req.headers["x-forwarded-host"] 145 | ? req.headers["x-forwarded-host"].split(".") 146 | : req.headers.host.split(":")[0].split("."); 147 | 148 | if (subdomain.length > 1 && subdomain[0] == "admin") { 149 | admin(req, res, next); 150 | } else { 151 | client(req, res, next); 152 | } 153 | } catch (e) { 154 | console.log("Bad request for " + req.headers.host + req.url, e); 155 | res.statusCode = 500; 156 | res.write("Bad request"); //write a response to the client 157 | res.end(); //end the response 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /server/public/layouts/client/embed.handlebars: -------------------------------------------------------------------------------- 1 | 9 |
    Loading..
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 | 17 | 18 | Artist 19 |
    20 |
    21 |
    22 | {{> spinner}} 23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 |
    31 | 34 |
    35 | play_arrow 36 | pause 37 |
    38 | 41 |
    00:00 / 00:00
    42 |
    43 | 44 | 45 | 46 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 | 59 | 60 | 61 | 62 | 01:00 63 | 64 | 65 | 66 | 67 | 68 | 69 |  votes 70 | 71 | 72 |
    73 |
    74 |
    75 |
    76 |
    77 | 78 | first_page 79 | 80 | 81 | first_page 82 | 83 | 84 | navigate_before prev 85 | 86 | 87 | navigate_before prev 88 | 89 | 1 90 | 91 | next navigate_next 92 | 93 | 94 | next navigate_next 95 | 96 | 97 | last_page 98 | 99 | 100 | last_page 101 | 102 |
    103 |
    104 |
    105 |
    106 | {{> spinner}} 107 |
    108 |
    109 |
    110 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"), 2 | uglify = require("gulp-uglify"), 3 | //sourcemaps = require('gulp-sourcemaps'), 4 | concat = require("gulp-concat"), 5 | cleanCSS = require("gulp-clean-css"); 6 | 7 | gulp.task("css", function() { 8 | return gulp 9 | .src([ 10 | "server/public/assets/css/style.css", 11 | "server/public/assets/css/globals.css", 12 | "server/public/assets/css/animations.css", 13 | "server/public/assets/css/mobile.css" 14 | ]) 15 | .pipe(concat("style.css")) 16 | .pipe(cleanCSS({ compatibility: "ie8" })) 17 | .pipe(gulp.dest("server/public/assets/dist")); 18 | }); 19 | 20 | gulp.task("css-embed", function() { 21 | return gulp 22 | .src("server/public/assets/css/embed.css") 23 | .pipe(cleanCSS({ compatibility: "ie8" })) 24 | .pipe(gulp.dest("server/public/assets/dist")); 25 | }); 26 | 27 | gulp.task("js", function() { 28 | return ( 29 | gulp 30 | .src([ 31 | "server/VERSION.js", 32 | "server/config/api_key.js", 33 | "server/public/assets/js/*.js", 34 | "!server/public/assets/js/embed*", 35 | "!server/public/assets/js/token*", 36 | "!server/public/assets/js/remotecontroller.js", 37 | "!server/public/assets/js/callback.js" 38 | ]) 39 | //.pipe(sourcemaps.init()) 40 | .pipe(concat("main.min.js")) 41 | .pipe( 42 | uglify({ 43 | mangle: true, 44 | compress: true, 45 | enclose: true 46 | }) 47 | ) 48 | //.pipe(sourcemaps.write('maps')) 49 | .pipe(gulp.dest("server/public/assets/dist")) 50 | ); 51 | }); 52 | 53 | gulp.task("embed", function() { 54 | return ( 55 | gulp 56 | .src([ 57 | "server/VERSION.js", 58 | "server/config/api_key.js", 59 | "server/public/assets/js/player.js", 60 | "server/public/assets/js/functions.js", 61 | "server/public/assets/js/helpers.js", 62 | "server/public/assets/js/playercontrols.js", 63 | "server/public/assets/js/list.js", 64 | "server/public/assets/js/embed.js", 65 | "!server/public/assets/js/frontpage*", 66 | "!server/public/assets/js/remotecontroller.js", 67 | "server/public/assets/js/hostcontroller.js" 68 | ]) 69 | //.pipe(sourcemaps.init()) 70 | .pipe(concat("embed.min.js")) 71 | .pipe( 72 | uglify({ 73 | mangle: true, 74 | compress: true, 75 | enclose: true 76 | }) 77 | ) 78 | //.pipe(sourcemaps.write('maps')) 79 | .pipe(gulp.dest("server/public/assets/dist")) 80 | ); 81 | }); 82 | 83 | gulp.task("token", function() { 84 | return ( 85 | gulp 86 | .src([ 87 | "server/public/assets/js/token*", 88 | "server/public/assets/js/helpers.js" 89 | ]) 90 | //.pipe(sourcemaps.init()) 91 | .pipe(concat("token.min.js")) 92 | .pipe( 93 | uglify({ 94 | mangle: true, 95 | compress: true, 96 | enclose: true 97 | }) 98 | ) 99 | //.pipe(sourcemaps.write('maps')) 100 | .pipe(gulp.dest("server/public/assets/dist")) 101 | ); 102 | }); 103 | 104 | gulp.task("callback", function() { 105 | return ( 106 | gulp 107 | .src([ 108 | "server/VERSION.js", 109 | "server/config/api_key.js", 110 | "server/public/assets/js/callback.js" 111 | ]) 112 | //.pipe(sourcemaps.init()) 113 | .pipe(concat("callback.min.js")) 114 | .pipe( 115 | uglify({ 116 | mangle: true, 117 | compress: true, 118 | enclose: true 119 | }) 120 | ) 121 | //.pipe(sourcemaps.write('maps')) 122 | .pipe(gulp.dest("server/public/assets/dist")) 123 | ); 124 | }); 125 | 126 | gulp.task("build", done => { 127 | gulp.series( 128 | "css", 129 | "css-embed", 130 | "js", 131 | "embed", 132 | "remotecontroller", 133 | "callback", 134 | "token" 135 | )(); 136 | done(); 137 | }); 138 | 139 | gulp.task("remotecontroller", function() { 140 | return ( 141 | gulp 142 | .src([ 143 | "server/VERSION.js", 144 | "server/config/api_key.js", 145 | "server/public/assets/js/remotecontroller.js", 146 | "server/public/assets/js/helpers.js" 147 | ]) 148 | ////.pipe(sourcemaps.init()) 149 | .pipe(concat("remote.min.js")) 150 | .pipe( 151 | uglify({ 152 | mangle: true, 153 | compress: true, 154 | enclose: true 155 | }) 156 | ) 157 | //.pipe(sourcemaps.write('maps')) 158 | .pipe(gulp.dest("server/public/assets/dist")) 159 | ); 160 | }); 161 | 162 | gulp.task("default", function() { 163 | gulp.watch(["server/VERSION.js", "server/public/assets/js/*.js"], ["js"]); 164 | gulp.watch(["server/public/assets/css/*.css"], ["css"]); 165 | gulp.watch(["server/public/assets/css/*.css"], ["css-embed"]); 166 | gulp.watch( 167 | ["server/public/assets/js/token*.js", "server/public/assets/js/helpers.js"], 168 | ["token"] 169 | ); 170 | gulp.watch(["server/VERSION.js", "server/public/assets/js/*.js"], ["embed"]); 171 | gulp.watch( 172 | [ 173 | "server/VERSION.js", 174 | "server/public/assets/js/callback.js", 175 | "server/public/assets/js/helpers.js" 176 | ], 177 | ["callback"] 178 | ); 179 | //gulp.watch('server/public/assets/js/*.js', ['nochan']); 180 | gulp.watch( 181 | ["server/VERSION.js", "server/public/assets/js/remotecontroller.js"], 182 | ["remotecontroller"] 183 | ); 184 | }); 185 | -------------------------------------------------------------------------------- /server/public/layouts/client/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | Zoff - the shared YouTube and SoundCloud based radio 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{#unless embed}} 27 | 28 | 29 | 30 | 31 | {{/unless}} 32 | 33 | 34 | 35 | {{#unless embed}} 36 | 50 | {{#if adds}} 51 | 52 | 58 | {{/if}} 59 | {{/unless}} 60 | 61 | 62 | {{{body}}} 63 | {{#unless embed}} 64 | {{#unless client}} 65 |
    66 |
    67 |
    Chromecast
    68 |

    This site supports chromecasting!

    69 |
    70 |
    71 | {{/unless}} 72 | {{> modal/cookie}} 73 | {{> contact}} 74 | {{> donate}} 75 | {{> footer}} 76 | 77 | 90 |
    91 | {{/unless}} 92 | 93 | 94 | 95 | {{#unless embed}} 96 | 97 | {{/unless}} 98 | 99 | 100 | -------------------------------------------------------------------------------- /server/EVENTS.md: -------------------------------------------------------------------------------- 1 | ## Events 2 | 3 | ### To server 4 | ``` 5 | // Tells the server the song is over 6 | 'end', { 7 | id: video_id, 8 | channel: channel_name, 9 | pass: Base64(channel_pass) 10 | } 11 | 12 | // Asks server where in the song it should be 13 | 'pos', { 14 | channel: channel_name, 15 | pass: Base64(hannel_pass) 16 | } 17 | 18 | // Tells the server the client wants the list 19 | 'list', { 20 | channel: channel_name, 21 | pass: Base64(channel_pass), 22 | version: system_version (can be checked in VERSION.js) 23 | } 24 | 25 | // Sends info about a song the client wants to add 26 | 'add', { 27 | id: VIDEO_ID, 28 | title: VIDEO_TITLE, 29 | adminpass: Base64(PASSWORD), 30 | duration: VIDEO_DURATION, 31 | list: channel_name, 32 | pass: Base64(channel_pass) 33 | } 34 | 35 | 'addPlaylist', { 36 | channel: CHANNEL_NAME, 37 | userpass: Base64(CHANNEL_PASSWORD), 38 | adminpass: Base64(PASSWORD), 39 | songs: [ 40 | { 41 | id: song_id, 42 | title: song_title, 43 | duration: song_duration 44 | }, ... { ... } 45 | ] 46 | } 47 | 48 | // Imports songs from another zoff-channel 49 | 'import_zoff', { 50 | channel: CHANNELNAME, 51 | new_channel: CHANNELNAME-TO-IMPORT-FROM, 52 | adminpass: Base64(PASSWORD), 53 | userpass: Bse64(CHANNEL_PASSWORD) 54 | } 55 | 56 | // Tells the server to disconnect the user from the current channel, is used for remote controlling on the host side 57 | 'change_channel', { 58 | channel: channel_name 59 | } 60 | 61 | // Sends chat text to all chat 62 | 'all,chat', { 63 | channel: channel_name, 64 | data: input 65 | } 66 | 67 | // Sends chat text to channelchat 68 | 'chat',{ 69 | channel: channel_name, 70 | data: input, 71 | pass: Base64(channel_pass) 72 | } 73 | 74 | // Sends info about song the user wants to vote on. If VOTE_TYPE is del, its deleting the song, if its pos, its just voting 75 | 'vote', { 76 | channel: CHANNEL_NAME, 77 | id: VIDEO_ID, 78 | type: VOTE_TYPE, 79 | adminpass: Base64(PASSWORD) 80 | } 81 | 82 | // Sends shuffle to the server (Only works every 5 seconds per list) 83 | 'shuffle', { 84 | adminpass: Base64(PASSWORD), 85 | channel: CHANNELNAME, 86 | pass: Base64(USER_PASSWORD) 87 | } 88 | 89 | // Sends skip message to server 90 | 'skip', { 91 | pass: Base64(PASSWORD), 92 | id:video_id, 93 | channel: chan, 94 | userpass: Base64(channel_pass) 95 | } 96 | 97 | // Sends password for instant log in to server 98 | 'password', { 99 | password: Base64(PASSWORD), 100 | channel: CHANNEL_NAME, 101 | oldpass: Base64(old_pass_if_changing_password) 102 | } 103 | 104 | // Sends message to the host channel for play 105 | 'id', { 106 | id: CHANNEL_ID, 107 | type: "play", 108 | value: "mock" 109 | } 110 | 111 | // Sends message to the host channel for pause 112 | 'id', { 113 | id: CHANNEL_ID, 114 | type: "pause", 115 | value: "mock" 116 | } 117 | 118 | // Sends message to the host channel for skip 119 | 'id', { 120 | id: CHANNEL_ID, 121 | type: "skip", 122 | value: "mock" 123 | } 124 | 125 | // Sends message to the host channel to change volume 126 | 'id', { 127 | id: CHANNEL_ID, 128 | type: "volume", 129 | value: VALUE 130 | } 131 | 132 | // Sends message to the host channel to change channel 133 | 'id', { 134 | id: CHANNEL_ID, 135 | type: "channel", 136 | value: NEW_CHANNEL_NAME 137 | } 138 | 139 | // Sends a video that triggered an error 140 | 'error_video', { 141 | channel: CHANNEL_NAME, 142 | id: VIDEO_ID, 143 | title: VIDEO_TITLE 144 | } 145 | 146 | // Requests chat-history from the last 10 minutes 147 | 'get_history', { 148 | channel: CHANNEL_NAME, 149 | all: BOOLEAN (if true, it requests for all-chat), 150 | pass: Base64(USERPASS) 151 | } 152 | ``` 153 | 154 | ### From server 155 | ``` 156 | // Receives a string from server for what type of toast to be triggered 157 | 'toast', STRING 158 | 159 | // Receives a boolean if the password was correct 160 | 'pw', BOOLEAN 161 | 162 | // Receives configuration array from server 163 | 'conf', [ARRAY] 164 | 165 | // Receives chat message from allchat 166 | 'chat.all', { 167 | from: name, 168 | msg: message, 169 | channel: channel, 170 | icon: icon_src 171 | } 172 | 173 | // Receives chat-history for all and for current channel 174 | 'chat_history', { 175 | all: BOOLEAN (if true, it is for all-chat), 176 | data: CHAT_HISTORY 177 | } 178 | 179 | // Receives chat message from channelchat 180 | 'chat', { 181 | from: name, 182 | msg: message, 183 | icon: icon_src 184 | } 185 | 186 | // Receives the ID of the current client, used for remote listening 187 | 'id', STRING 188 | 189 | // Receives the messages sent on CHANNEL_ID above 190 | id, { 191 | type: STRING, 192 | value: VALUE 193 | } 194 | 195 | // Receives updates from channel. type is one of the following: list, added, deleted, vote, song_change, changed_values (see further down for better explanation here) 196 | 'channel', { 197 | type: TYPE, 198 | value: value, 199 | time: time_of_occurence 200 | } 201 | 202 | // Receives message from the server that its ready to send the playlist and info 203 | 'get_list' 204 | 205 | // Receives array of now playing song. Is triggered on song-change 206 | 'np', { 207 | np: NOW_PLAYING, 208 | conf: CONFIGURATION, 209 | time: SERVER_TIME 210 | } 211 | 212 | // Receives number of viewers on the current channel 213 | 'viewers', VALUE 214 | 215 | // Receives a newly updated video, that was checked for errors (song_generated contains .id which is the current id of the video, and a .new_id for the new video to change the video to) 216 | 'channel', { 217 | type: "changed_values", 218 | value: song_generated 219 | } 220 | 221 | 'update_required', { 222 | description of what is wrong as an object 223 | } 224 | ``` 225 | -------------------------------------------------------------------------------- /server/public/assets/html/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Zoff OAuth Callback 5 | 6 | 7 | 8 | 170 | 171 | 172 |

    Loading..

    173 |
    174 | 175 | 176 | -------------------------------------------------------------------------------- /server/public/assets/js/suggestions.js: -------------------------------------------------------------------------------- 1 | var Suggestions = { 2 | catchUserSuggests: function(params, single) { 3 | if (single) { 4 | number_suggested = number_suggested + 1; 5 | } else { 6 | number_suggested = number_suggested + params.length; 7 | } 8 | for (var i = 0; i < params.length; i++) { 9 | if (document.querySelectorAll("#suggested-" + params[i].id).length > 0) { 10 | number_suggested -= 1; 11 | } 12 | } 13 | var to_display = number_suggested > 9 ? "9+" : number_suggested; 14 | if (number_suggested > 0 && Admin.logged_in) { 15 | Helper.removeClass( 16 | document.querySelector(".suggested-link span.badge.new.white"), 17 | "hide" 18 | ); 19 | } 20 | document.querySelector( 21 | ".suggested-link span.badge.new.white" 22 | ).innerText = to_display; 23 | if (single) { 24 | Suggestions.createSuggested(params); 25 | } else { 26 | for (var x in params) { 27 | Suggestions.createSuggested(params[x]); 28 | } 29 | } 30 | Suggestions.checkUserEmpty(); 31 | }, 32 | 33 | createSuggested: function(params) { 34 | var duration = Helper.secondsToOther(params.duration); 35 | var video_id = params.id; 36 | var video_title = params.title; 37 | var date = new Date(params.added * 1000); 38 | var addedTime = 39 | Helper.pad(date.getDate()) + 40 | "." + 41 | Helper.pad(date.getMonth()) + 42 | "." + 43 | Helper.pad(date.getYear() - 100); 44 | var toSend = { 45 | id: video_id, 46 | title: video_title, 47 | length: params.duration, 48 | duration: duration, 49 | votes: addedTime, 50 | extra: "Added" 51 | }; 52 | if (params.added_by != undefined) { 53 | toSend.extra += " by " + params.added_by; 54 | } 55 | if (params.source) toSend.source = params.source; 56 | else { 57 | toSend.source = "youtube"; 58 | } 59 | if (params.thumbnail) toSend.thumbnail = params.thumbnail; 60 | var song = List.generateSong(toSend, false, false, false, true); 61 | var testingElem; 62 | try { 63 | testingElem = document.getElementById(video_id); 64 | } catch (e) {} 65 | 66 | if ( 67 | !testingElem && 68 | document.querySelectorAll("#suggested-" + video_id).length == 0 69 | ) { 70 | document 71 | .getElementById("user-suggest-html") 72 | .insertAdjacentHTML("beforeend", song); 73 | } 74 | }, 75 | 76 | fetchYoutubeSuggests: function(id) { 77 | if (videoSource == "soundcloud") { 78 | Helper.addClass(document.querySelector(".suggest-title-info"), "hide"); 79 | Helper.addClass("#suggest-song-html", "hide"); 80 | return; 81 | } else { 82 | Helper.removeClass(document.querySelector(".suggest-title-info"), "hide"); 83 | Helper.removeClass("#suggest-song-html", "hide"); 84 | } 85 | var previousSuggest = window.localStorage.getItem("youtube-suggests"); 86 | if ( 87 | previousSuggest == id && 88 | document.getElementById("suggest-song-html").innerHTML.trim() != 89 | "Loading.." 90 | ) { 91 | return; 92 | } 93 | document.getElementById("suggest-song-html").innerHTML = "Loading..."; 94 | window.localStorage.setItem("youtube-suggests", id); 95 | var get_url = 96 | "https://www.googleapis.com/youtube/v3/search?part=snippet&relatedToVideoId=" + 97 | id + 98 | "&type=video&key=" + 99 | getYoutubeKey(); 100 | var video_urls = 101 | "https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet,id,statistics&fields=pageInfo,items(id,contentDetails,statistics(viewCount),snippet(categoryId,channelTitle,publishedAt,title,description,thumbnails))&key=" + 102 | getYoutubeKey() + 103 | "&id="; 104 | 105 | Helper.ajax({ 106 | type: "GET", 107 | url: get_url, 108 | dataType: "jsonp", 109 | success: function(response) { 110 | response = JSON.parse(response); 111 | var this_resp = response.items.slice(0, 5); 112 | for (var i = 0; i < this_resp.length; i++) { 113 | var data = this_resp[i]; 114 | video_urls += data.id.videoId + ","; 115 | } 116 | 117 | Helper.ajax({ 118 | type: "GET", 119 | url: video_urls, 120 | dataType: "jsonp", 121 | success: function(response) { 122 | response = JSON.parse(response); 123 | Helper.setHtml("#suggest-song-html", ""); 124 | for (var i = 0; i < response.items.length; i++) { 125 | var song = response.items[i]; 126 | var duration = song.contentDetails.duration; 127 | var length = Search.durationToSeconds(duration); 128 | duration = Helper.secondsToOther( 129 | Search.durationToSeconds(duration) 130 | ); 131 | var video_id = song.id; 132 | var video_title = song.snippet.title; 133 | var viewCount = 0; 134 | try { 135 | viewCount = song.statistics.viewCount 136 | .toString() 137 | .replace(/\B(?=(\d{3})+(?!\d))/g, " "); 138 | } catch (e) {} 139 | 140 | try { 141 | document.getElementById("suggest-song-html").insertAdjacentHTML( 142 | "beforeend", 143 | List.generateSong( 144 | { 145 | id: video_id, 146 | title: video_title, 147 | length: length, 148 | duration: duration, 149 | votes: viewCount, 150 | extra: "Views", 151 | source: "youtube" 152 | }, 153 | false, 154 | false, 155 | false 156 | ) 157 | ); 158 | } catch (e) {} 159 | } 160 | } 161 | }); 162 | } 163 | }); 164 | }, 165 | 166 | checkUserEmpty: function() { 167 | var length = document.getElementById("user-suggest-html").children.length; 168 | if (length === 0) { 169 | Helper.addClass("#user_suggests", "hide"); 170 | } else if (Admin.logged_in) { 171 | Helper.removeClass("#user_suggests", "hide"); 172 | } 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /server/REST.md: -------------------------------------------------------------------------------- 1 | ## REST 2 | 3 | All PUT, DELETE and POST endpoints have a 1-second waitlimit for each command per client. You'll get a response with Retry-After header for how long you have to wait. Shuffling in a player has a 5-second waitlimit, but per channel instead of per client. 4 | 5 | If you want to skip the wait-times, create a token at https://zoff.me/api/apply. Tokens are added to all the POST, PUT, DELETE, requests as ``` token: TOKEN ```. 6 | 7 | All requests return things on this form (results field is added if successful.) 8 | 9 | ``` 10 | { 11 | status: STATUSCODE, 12 | error: MESSAGE, 13 | success: IF_SUCCESSFULL, 14 | results: [RESULTS] (if something went wrong, there is one element in this array. This tells you what is wrong with the request, and what was expected) 15 | } 16 | ``` 17 | 18 | Add song 19 | 20 | ``` 21 | POST /api/list/:channel_name/:video_id 22 | { 23 | "title": TITLE, 24 | "duration": END_TIME - START_TIME, 25 | "end_time": END_TIME, 26 | "start_time": START_TIME, 27 | "adminpass": PASSWORD, (leave this blank if there is no password/you don't know the password) 28 | "userpass": USER_PASSWORD 29 | "source": Either "youtube" or "soundcloud" 30 | ("thumbnail": thumbnail url for soundcloud elements (only used when source == "soundcloud")) 31 | } 32 | 33 | Returns 400 for bad request 34 | Returns 403 for bad authentication (but will return a song object, with the type == "suggested", and the song will show up in the suggested tab for channel-admins) 35 | Returns 409 if the song exists 36 | Returns 429 if you're doing too much of this request, with a Retry-After int value in the header. 37 | Returns 200 and the added song object if successful 38 | ``` 39 | 40 | Delete song 41 | ``` 42 | DELETE /api/list/:channel_name/:video_id 43 | { 44 | "adminpass": PASSWORD, 45 | "userpass": USER_PASSWORD 46 | } 47 | 48 | Returns 400 for bad request 49 | Returns 403 for bad authentication 50 | Returns 404 if the song doesnt exist or is the currently playing song 51 | Returns 429 if you're doing too much of this request, with a Retry-After int value in the header. 52 | Returns 200 if successful 53 | ``` 54 | 55 | Vote on song 56 | ``` 57 | PUT /api/list/:channel_name/:video_id 58 | { 59 | "adminpass": PASSWORD, 60 | "userpass": USER_PASSWORD 61 | } 62 | 63 | Returns 400 for bad request 64 | Returns 403 for bad authentication 65 | Returns 404 if the song doesnt exist 66 | Returns 409 if you've already voted on that song 67 | Returns 429 if you're doing too much of this request, with a Retry-After int value in the header. 68 | Returns 200 and the added song object if successful 69 | ``` 70 | 71 | Change channel configurations 72 | ``` 73 | PUT /api/conf/:channel_name 74 | { 75 | "userpass": USER_PASSWORD, 76 | "adminpass": PASSWORD, 77 | "vote": BOOLEAN, 78 | "addsongs": BOOLEAN, 79 | "longsongs": BOOLEAN, 80 | "frontpage": BOOLEAN (if you want to set userpassword, this MUST be false for it to work), 81 | "allvideos": BOOLEAN, 82 | "removeplay": BOOLEAN, 83 | "skip": BOOLEAN, 84 | "shuffle": BOOLEAN, 85 | "userpass_changed": BOOLEAN (this must be true if you want to keep the userpassword you're sending) 86 | } 87 | 88 | Returns 400 for bad request 89 | Returns 403 for bad authentication 90 | Returns 404 if the list doesn't exist 91 | Returns 429 if you're doing too much of this request, with a Retry-After int value in the header. 92 | Returns 200 and the newly added configuration if successful 93 | ``` 94 | 95 | Get song in channel 96 | ``` 97 | GET /api/list/:channel_name/:video_id 98 | 99 | Returns 403 for bad authentication (if you get this, the channel is protected, try getting the full channel with POST, and search through the object) 100 | Returns 404 if the song doesn't exist 101 | Returns 200 and the song 102 | ``` 103 | 104 | Get song in channel (protected) 105 | ``` 106 | // Important fetch_song is present, or else the request will try to add a song to the channel 107 | POST /api/list/:channel_name/:video_id 108 | { 109 | "fetch_song": ANYTHING_HERE, 110 | "userpass": USERPASS 111 | } 112 | 113 | Returns 400 for bad request 114 | Returns 403 for bad authentication 115 | Returns 404 if the song doesn't exist 116 | Returns 200 and the song 117 | ``` 118 | 119 | Get list 120 | ``` 121 | GET /api/list/:channel_name/ 122 | 123 | Returns 403 for bad authentication (if you get this, the channel is protected, try getting the full channel with POST, and search through the object) 124 | Returns 404 if the song doesn't exist 125 | Returns 200 and the song 126 | ``` 127 | 128 | Get list (protected) 129 | ``` 130 | // Important fetch_song is present, or else the request will try to add a song to the channel 131 | POST /api/list/:channel_name/ 132 | { 133 | "userpass": USERPASS 134 | } 135 | 136 | Returns 400 for bad request 137 | Returns 403 for bad authentication 138 | Returns 404 if the list doesn't exist 139 | Returns 200 and the song 140 | ``` 141 | 142 | Get channelsettings 143 | ``` 144 | GET /api/conf/:channel_name/ 145 | 146 | Returns 403 for bad authentication (if you get this, try POST with userpassword attached) 147 | Returns 404 if the channel doesn't exist 148 | Returns 200 and the objects in the channel 149 | ``` 150 | 151 | Get channelsettings (protected) 152 | ``` 153 | POST /api/conf/:channel_name/ 154 | { 155 | "userpass": USERPASS 156 | } 157 | 158 | Returns 400 for bad request 159 | Returns 403 for bad authentication 160 | Returns 404 if the channel doesn't exist 161 | Returns 200 and the objects in the channel 162 | ``` 163 | 164 | Get now playing song 165 | ``` 166 | GET /api/list/:channel_name/__np__ 167 | 168 | Returns 400 for bad request 169 | Returns 403 for bad authentication (if you get this, try POST with userpassword attached) 170 | Returns 404 if the channel doesn't exist 171 | Returns 200 and the now playing object 172 | ``` 173 | 174 | Get now playing song (protected) 175 | ``` 176 | POST /api/list/:channel_name/__np__ 177 | { 178 | "userpass": USERPASS 179 | } 180 | 181 | Returns 400 for bad request 182 | Returns 403 for bad authentication (if you get this, try POST with userpassword attached) 183 | Returns 404 if the channel doesn't exist 184 | Returns 200 and the now playing object 185 | ``` 186 | 187 | Get all lists 188 | ``` 189 | GET /api/frontpages 190 | 191 | Returns 200 and the frontpage-lists 192 | ``` 193 | -------------------------------------------------------------------------------- /server/apps/client.js: -------------------------------------------------------------------------------- 1 | VERSION = require(pathThumbnails + "/VERSION.js"); 2 | var secure = false; 3 | var path = require("path"); 4 | try { 5 | var cert_config = require(path.join( 6 | path.join(__dirname, "../config/"), 7 | "cert_config.js" 8 | )); 9 | var fs = require("fs"); 10 | var privateKey = fs.readFileSync(cert_config.privateKey).toString(); 11 | var certificate = fs.readFileSync(cert_config.certificate).toString(); 12 | var ca = fs.readFileSync(cert_config.ca).toString(); 13 | var credentials = { 14 | key: privateKey, 15 | cert: certificate, 16 | ca: ca 17 | }; 18 | secure = true; 19 | } catch (err) {} 20 | 21 | var add = ""; 22 | var express = require("express"); 23 | var app = express(); 24 | var compression = require("compression"); 25 | var exphbs = require("express-handlebars"); 26 | var cors = require("cors"); 27 | var Functions = require(pathThumbnails + "/handlers/functions.js"); 28 | 29 | var hbs = exphbs.create({ 30 | defaultLayout: publicPath + "/layouts/client/main", 31 | layoutsDir: publicPath + "/layouts/client", 32 | partialsDir: publicPath + "/partials", 33 | helpers: { 34 | if_equal: function(a, b, opts) { 35 | if (a == b) { 36 | return opts.fn(this); 37 | } else { 38 | return opts.inverse(this); 39 | } 40 | }, 41 | decodeString: function(s) { 42 | if (s == undefined) return s; 43 | return Functions.decodeChannelName(s); 44 | } 45 | } 46 | }); 47 | var uniqid = require("uniqid"); 48 | app.use(compression({ filter: shouldCompress })); 49 | 50 | function shouldCompress(req, res) { 51 | if (req.headers["x-no-compression"]) { 52 | // don't compress responses with this request header 53 | return false; 54 | } 55 | 56 | // fallback to standard filter function 57 | return compression.filter(req, res); 58 | } 59 | 60 | app.engine("handlebars", hbs.engine); 61 | app.set("view engine", "handlebars"); 62 | app.enable("view cache"); 63 | app.set("views", publicPath); 64 | app.set("trust proxy", "127.0.0.1"); 65 | 66 | var bodyParser = require("body-parser"); 67 | var cookieParser = require("cookie-parser"); 68 | var referrerPolicy = require("referrer-policy"); 69 | var helmet = require("helmet"); 70 | var featurePolicy = require("feature-policy"); 71 | app.use( 72 | featurePolicy({ 73 | features: { 74 | fullscreen: ["*"], 75 | //vibrate: ["'none'"], 76 | payment: ["'none'"], 77 | microphone: ["'none'"], 78 | camera: ["'none'"], 79 | speaker: ["*"], 80 | syncXhr: ["'self'"] 81 | //notifications: ["'self'"] 82 | } 83 | }) 84 | ); 85 | app.use( 86 | helmet({ 87 | frameguard: false 88 | }) 89 | ); 90 | app.use(referrerPolicy({ policy: "origin-when-cross-origin" })); 91 | app.use(bodyParser.json()); // to support JSON-encoded bodies 92 | app.use( 93 | bodyParser.urlencoded({ 94 | // to support URL-encoded bodies 95 | extended: true 96 | }) 97 | ); 98 | app.use(cookieParser()); 99 | //app.set('json spaces', 2); 100 | 101 | io = require("socket.io")({ 102 | pingTimeout: 25000, 103 | cookie: false 104 | //path: '/zoff', 105 | //"origins": ("https://zoff.me:443*,https://zoff.me:8080*,zoff.me:8080*,https://remote.zoff.me:443*,https://remote.zoff.me:8080*,https://fb.zoff.me:443*,https://fb.zoff.me:8080*,https://admin.zoff.me:443*,https://admin.zoff.me:8080*, http://localhost:8080*")}); 106 | }); 107 | 108 | var socketIO = require(pathThumbnails + "/handlers/io.js"); 109 | socketIO(); 110 | 111 | app.socketIO = io; 112 | 113 | /* Globally needed "libraries" and files */ 114 | var router = require(pathThumbnails + "/routing/client/router.js"); 115 | var api_file = require(pathThumbnails + "/routing/client/api.js"); 116 | var api = api_file.router; 117 | api_file.sIO = app.socketIO; 118 | var ico_router = require(pathThumbnails + "/routing/client/icons_routing.js"); 119 | 120 | app.get("/robots.txt", function(req, res) { 121 | res.type("text/plain"); 122 | res.send("User-agent: *\nAllow: /$\nDisallow: /"); 123 | }); 124 | 125 | function getOrigin(req) { 126 | var origin = "*"; 127 | try { 128 | origin = req.get("origin"); 129 | } catch (e) {} 130 | return origin; 131 | } 132 | 133 | app.use(function(req, res, next) { 134 | var cookie = req.cookies._uI; 135 | var skipElements = [ 136 | "/_embed", 137 | "/assets/manifest.json", 138 | "/apple-touch-icon.png" 139 | ]; 140 | if (skipElements.indexOf(req.originalUrl) > -1) { 141 | var origin = getOrigin(req); 142 | res.header("Access-Control-Allow-Origin", origin); 143 | res.header( 144 | "Access-Control-Allow-Headers", 145 | "Origin, X-Requested-With, Content-Type, Accept" 146 | ); 147 | next(); 148 | } else { 149 | if (req.originalUrl.split("/").length > 3) { 150 | var origin = getOrigin(req); 151 | res.header("Access-Control-Allow-Origin", origin); 152 | res.header( 153 | "Access-Control-Allow-Headers", 154 | "Origin, X-Requested-With, Content-Type, Accept" 155 | ); 156 | next(); 157 | } else { 158 | if (cookie === undefined) { 159 | try { 160 | //console.error((new Date), "originalUrl", req.originalUrl); 161 | //console.error((new Date), "couldn't fetch cookie for some reason, maybe no cookie exists?", req.get('origin'), "couldn't fetch cookie for some reason, maybe no cookie exists?"); 162 | } catch (e) { 163 | //console.error((new Date), "couldn't fetch origin"); 164 | } 165 | var user_name = Functions.hash_pass( 166 | Functions.rndName(uniqid.time(), 15) 167 | ); 168 | res.cookie("_uI", user_name, { 169 | maxAge: 365 * 10000 * 3600000, 170 | httpOnly: true, 171 | secure: secure, 172 | sameSite: "None" 173 | }); 174 | } else { 175 | //process.stderr.write((new Date), "couldn't fetch cookie for some reason, maybe no cookie exists?", req, "couldn't fetch cookie for some reason, maybe no cookie exists?"); 176 | res.cookie("_uI", cookie, { 177 | maxAge: 365 * 10000 * 3600000, 178 | httpOnly: true, 179 | secure: secure, 180 | sameSite: "None" 181 | }); 182 | } 183 | var origin = getOrigin(req); 184 | res.header("Access-Control-Allow-Origin", origin); 185 | res.header( 186 | "Access-Control-Allow-Headers", 187 | "Origin, X-Requested-With, Content-Type, Accept" 188 | ); 189 | next(); 190 | } 191 | } 192 | }); 193 | 194 | app.use("/service-worker.js", function(req, res) { 195 | res.sendFile(publicPath + "/service-worker.js"); 196 | }); 197 | 198 | app.use("/", ico_router); 199 | app.use("/", api); 200 | app.use("/", cors(), router); 201 | 202 | app.use("/assets/js", function(req, res, next) { 203 | res.sendStatus(403); 204 | return; 205 | }); 206 | 207 | app.use("/assets/admin", function(req, res, next) { 208 | res.sendStatus(403); 209 | return; 210 | }); 211 | 212 | app.use("/assets", express.static(publicPath + "/assets")); 213 | 214 | app.use(function(req, res, next) { 215 | res.status(404); 216 | res.redirect("/404"); 217 | }); 218 | 219 | module.exports = app; 220 | -------------------------------------------------------------------------------- /server/public/partials/channel/settings.handlebars: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | Channel Settings 4 | tune 5 |
    6 |
    7 |
      8 | 9 |
      10 |
    • 11 |
      12 | lock 13 | 14 |
      15 |
    • 16 |
      17 |
      18 |
    • 19 | 20 | Add songs 21 | 22 |
      23 | 28 |
      29 |
    • 30 |
    • 31 | 32 | Vote 33 | 34 |
      35 | 40 |
      41 |
    • 42 | 43 |
    • 44 | 45 | Shuffle 46 | 47 |
      48 | 53 |
      54 |
    • 55 |
    • 56 | 57 | Skip 58 | 59 |
      60 | 65 |
      66 |
    • 67 |
    • 68 | 69 | Song length 70 | 71 |
      72 | 77 |
      78 |
    • 79 |
    • 80 | 81 | Type 82 | 83 |
      84 | 89 |
      90 |
    • 91 |
    • 92 | 93 | Frontpage 94 | 95 |
      96 | 101 |
      102 |
    • 103 |
    • 104 | 105 | After play 106 | 107 |
      108 | 113 |
      114 |
    • 115 |
    • 116 | 117 | Strict skip 118 | 119 |
      120 | 125 |
      126 |
    • 127 |
    • 128 | 129 | Chat 130 | 131 |
      132 | 137 |
      138 |
    • 139 |
    • 140 | 141 | Password 142 | 143 |
      144 | 149 |
      150 |
    • 151 |
      152 |
      153 |
    • 154 |
      155 | queue_play_next 156 | 157 |
      votes needed to skip.
      158 |
      159 |
    • 160 |
      161 |
    • 162 | Change password 163 |
    • 164 |
    • 165 | Delete all songs 166 |
    • 167 |
    168 |
    169 |
  • 170 | -------------------------------------------------------------------------------- /server/public/partials/channel/modal.handlebars: -------------------------------------------------------------------------------- 1 | {{#unless client}} 2 | 34 | 41 | {{/unless}} 42 | 67 | 83 | 120 | 135 | 145 | -------------------------------------------------------------------------------- /server/apps/admin.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var app = express(); 3 | 4 | const path = require("path"); 5 | const publicPath = path.join(__dirname + "", "../public"); 6 | var exphbs = require("express-handlebars"); 7 | var hbs = exphbs.create({ 8 | defaultLayout: publicPath + "/layouts/admin/main", 9 | layoutsDir: publicPath + "/layouts", 10 | partialsDir: publicPath + "/partials" 11 | }); 12 | 13 | var passport = require("passport"); 14 | var mpromise = require("mpromise"); 15 | var LocalStrategy = require("passport-local").Strategy; 16 | var mongoose = require("mongoose"); 17 | var mongo_db_cred = require(pathThumbnails + "/config/mongo_config.js"); 18 | var mongojs = require("mongojs"); 19 | var db = mongojs(mongo_db_cred.config); 20 | var token_db = mongojs("tokens"); 21 | var bodyParser = require("body-parser"); 22 | var session = require("express-session"); 23 | var MongoStore = require("connect-mongo")(session); 24 | var api = require(pathThumbnails + "/routing/admin/api.js"); 25 | 26 | var compression = require("compression"); 27 | var User = require(pathThumbnails + "/models/user.js"); 28 | var url = "mongodb://" + mongo_db_cred.host + "/" + mongo_db_cred.users; 29 | mongoose.connect(url); 30 | 31 | app.engine("handlebars", hbs.engine); 32 | app.set("view engine", "handlebars"); 33 | app.use(compression({ filter: shouldCompress })); 34 | 35 | function shouldCompress(req, res) { 36 | if (req.headers["x-no-compression"]) { 37 | // don't compress responses with this request header 38 | return false; 39 | } 40 | 41 | // fallback to standard filter function 42 | return compression.filter(req, res); 43 | } 44 | app.set("trust proxy", "127.0.0.1"); 45 | 46 | var bodyParser = require("body-parser"); 47 | var cookieParser = require("cookie-parser"); 48 | var referrerPolicy = require("referrer-policy"); 49 | var helmet = require("helmet"); 50 | var featurePolicy = require("feature-policy"); 51 | app.use( 52 | featurePolicy({ 53 | features: { 54 | fullscreen: ["*"], 55 | //vibrate: ["'none'"], 56 | payment: ["'none'"], 57 | microphone: ["'none'"], 58 | camera: ["'none'"], 59 | speaker: ["*"], 60 | syncXhr: ["'self'"] 61 | //notifications: ["'self'"] 62 | } 63 | }) 64 | ); 65 | app.use( 66 | helmet({ 67 | frameguard: false 68 | }) 69 | ); 70 | app.use(referrerPolicy({ policy: "origin-when-cross-origin" })); 71 | app.enable("view cache"); 72 | app.set("views", publicPath); 73 | app.use(bodyParser.json()); // to support JSON-encoded bodies 74 | app.use( 75 | bodyParser.urlencoded({ 76 | extended: true 77 | }) 78 | ); 79 | app.use( 80 | session({ 81 | secret: mongo_db_cred.secret, 82 | resave: true, 83 | saveUninitialized: true, 84 | store: new MongoStore({ 85 | url: url, 86 | useNewUrlParser: true, 87 | collection: "sessions", 88 | ttl: mongo_db_cred.expire 89 | }) 90 | }) 91 | ); // session secret 92 | app.use(passport.initialize()); 93 | app.use(passport.session()); // persistent login sessions 94 | 95 | //app.use('/assets', express.static(publicPath + '/assets')); 96 | 97 | passport.serializeUser(function(user, done) { 98 | done(null, user.id); 99 | }); 100 | 101 | // used to deserialize the user 102 | passport.deserializeUser(function(id, done) { 103 | User.findById(id, function(err, user) { 104 | done(err, user); 105 | }); 106 | }); 107 | 108 | passport.use( 109 | "local-signup", 110 | new LocalStrategy( 111 | { 112 | // by default, local strategy uses username and password, we will override with username 113 | usernameField: "username", 114 | passwordField: "password", 115 | passReqToCallback: true // allows us to pass back the entire request to the callback 116 | }, 117 | function(req, username, password, done) { 118 | // asynchronous 119 | // User.findOne wont fire unless data is sent back 120 | process.nextTick(function() { 121 | // find a user whose username is the same as the forms username 122 | // we are checking to see if the user trying to login already exists 123 | var token = req.body.token; 124 | token_db 125 | .collection("tokens") 126 | .find({ token: token }, function(err, docs) { 127 | if (docs.length == 1) { 128 | token_db 129 | .collection("tokens") 130 | .remove({ token: token }, function(err, docs) { 131 | User.findOne({ username: username }, function(err, user) { 132 | // if there are any errors, return the error 133 | if (err) return done(err); 134 | 135 | // check to see if theres already a user with that username 136 | if (user) { 137 | return done(null, false); 138 | } else { 139 | // if there is no user with that username 140 | // create the user 141 | var newUser = new User(); 142 | 143 | // set the user's local credentials 144 | newUser.username = username; 145 | newUser.password = newUser.generateHash(password); 146 | 147 | // save the user 148 | newUser.save(function(err) { 149 | if (err) throw err; 150 | return done(null, newUser); 151 | }); 152 | } 153 | }); 154 | }); 155 | } else { 156 | return done(null, false); 157 | } 158 | }); 159 | }); 160 | } 161 | ) 162 | ); 163 | 164 | passport.use( 165 | "local-login", 166 | new LocalStrategy( 167 | { 168 | // by default, local strategy uses username and password, we will override with email 169 | usernameField: "username", 170 | passwordField: "password", 171 | passReqToCallback: true // allows us to pass back the entire request to the callback 172 | }, 173 | function(req, username, password, done) { 174 | // callback with email and password from our form 175 | 176 | // find a user whose email is the same as the forms email 177 | // we are checking to see if the user trying to login already exists 178 | User.findOne({ username: username }, function(err, user) { 179 | // if there are any errors, return the error before anything else 180 | if (err) return done(err); 181 | 182 | // if no user is found, return the message 183 | if (!user) return done(null, false); // req.flash is the way to set flashdata using connect-flash 184 | 185 | // if the user is found but the password is wrong 186 | if (!user.validPassword(password)) return done(null, false); // create the loginMessage and save it to session as flashdata 187 | 188 | // all is well, return successful user 189 | 190 | return done(null, user); 191 | }); 192 | } 193 | ) 194 | ); 195 | 196 | app.post( 197 | "/signup", 198 | passport.authenticate("local-signup", { 199 | successRedirect: "/", // redirect to the secure profile section 200 | failureRedirect: "/signup", // redirect back to the signup page if there is an error 201 | failureFlash: true // allow flash messages 202 | }) 203 | ); 204 | 205 | app.post( 206 | "/login", 207 | passport.authenticate("local-login", { 208 | successRedirect: "/", // redirect to the secure profile section 209 | failureRedirect: "/login#failed", // redirect back to the signup page if there is an error 210 | failureFlash: true // allow flash messages 211 | }) 212 | ); 213 | 214 | app.use("/login", isLoggedInTryingToLogIn, function(req, res) { 215 | var data = { 216 | where_get: "not_authenticated" 217 | }; 218 | 219 | res.render("layouts/admin/not_authenticated", data); 220 | }); 221 | 222 | app.use("/signup", isLoggedInTryingToLogIn, function(req, res) { 223 | var data = { 224 | where_get: "not_authenticated" 225 | }; 226 | 227 | res.render("layouts/admin/not_authenticated", data); 228 | }); 229 | 230 | app.use("/", api); 231 | 232 | app.use("/logout", function(req, res) { 233 | req.logout(); 234 | res.redirect("/login"); 235 | }); 236 | 237 | app.use("/assets/admin/authenticated", function(req, res, next) { 238 | if (!req.isAuthenticated()) { 239 | res.sendStatus(403); 240 | return; 241 | } 242 | return next(); 243 | }); 244 | 245 | app.use("/assets", express.static(publicPath + "/assets")); 246 | 247 | app.use("/", isLoggedIn, function(req, res) { 248 | var data = { 249 | where_get: "authenticated", 250 | year: new Date().getYear() + 1900 251 | }; 252 | 253 | res.render("layouts/admin/authenticated", data); 254 | }); 255 | 256 | function isLoggedInTryingToLogIn(req, res, next) { 257 | if (!req.isAuthenticated()) { 258 | return next(); 259 | } 260 | res.redirect("/"); 261 | } 262 | 263 | function isLoggedIn(req, res, next) { 264 | if (req.isAuthenticated()) return next(); 265 | res.redirect("/login"); 266 | } 267 | 268 | //app.listen(default_port); 269 | 270 | module.exports = app; 271 | -------------------------------------------------------------------------------- /server/routing/client/router.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | var path = require("path"); 4 | var year = new Date().getYear() + 1900; 5 | var path = require("path"); 6 | var analytics = "xx"; 7 | var google = {}; 8 | var adsense = "xx"; 9 | var adds = false; 10 | var mongojs = require("mongojs"); 11 | var token_db = mongojs("tokens"); 12 | var Functions = require(pathThumbnails + "/handlers/functions.js"); 13 | var Frontpage = require(pathThumbnails + "/handlers/frontpage.js"); 14 | 15 | var db = require(pathThumbnails + "/handlers/db.js"); 16 | 17 | try { 18 | google = require(path.join( 19 | path.join(__dirname, "../../config/"), 20 | "google.js" 21 | )); 22 | analytics = google.analytics; 23 | adsense = google.adsense; 24 | } catch (e) { 25 | console.log( 26 | "(!) Missing file - /config/google.js Have a look at /config/google.example.js. This is for google analytics." 27 | ); 28 | } 29 | 30 | try { 31 | var Recaptcha = require("express-recaptcha"); 32 | var recaptcha_config = require(path.join( 33 | path.join(__dirname, "../../config/"), 34 | "recaptcha.js" 35 | )); 36 | var RECAPTCHA_SITE_KEY = recaptcha_config.site; 37 | var RECAPTCHA_SECRET_KEY = recaptcha_config.key; 38 | var recaptcha = new Recaptcha(RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY); 39 | } catch (e) { 40 | console.log( 41 | "(!) Missing file - /config/recaptcha.js Have a look at /config/recaptcha.example.js." 42 | ); 43 | var recaptcha = { 44 | middleware: { 45 | render: (req, res, next) => { 46 | res.recaptcha = ""; 47 | next(); 48 | } 49 | } 50 | }; 51 | } 52 | 53 | router.use(recaptcha.middleware.render, function(req, res, next) { 54 | next(); // make sure we go to the next routes and don't stop here 55 | }); 56 | 57 | router.route("/:channel_name").get(function(req, res, next) { 58 | channel(req, res, next); 59 | }); 60 | 61 | router.route("/r/:base64data").get(function(req, res, next) { 62 | var channelToRedirect = Buffer.from(req.params.base64data, "base64"); 63 | res.redirect("/" + channelToRedirect); 64 | }); 65 | 66 | router.route("/").get(function(req, res, next) { 67 | root(req, res, next); 68 | }); 69 | 70 | router.route("/").post(function(req, res, next) { 71 | root(req, res, next); 72 | }); 73 | 74 | router.route("/api/embed").get(function(req, res, next) { 75 | var data = { 76 | year: year, 77 | type: "video", 78 | javascript_file: "embed.min.js", 79 | captcha: res.recaptcha, 80 | analytics: analytics, 81 | stylesheet: "embed.css", 82 | embed: true, 83 | og_image: "https://zoff.me/assets/images/small-square.jpg" 84 | }; 85 | res.render("layouts/client/embed", data); 86 | }); 87 | 88 | router.route("/api/oauth").get(function(req, res, next) { 89 | res.sendFile(path.join(pathThumbnails, "/public/assets/html/callback.html")); 90 | }); 91 | 92 | router.route("/api/apply").get(function(req, res, next) { 93 | var data = { 94 | year: year, 95 | javascript_file: "token.min.js", 96 | captcha: res.recaptcha, 97 | analytics: analytics, 98 | adsense: adsense, 99 | adds: adds, 100 | type: "website", 101 | activated: false, 102 | id: "", 103 | correct: false, 104 | stylesheet: "style.css", 105 | embed: false, 106 | og_image: "https://zoff.me/assets/images/small-square.jpg" 107 | }; 108 | res.render("layouts/client/token", data); 109 | }); 110 | 111 | router.route("/api/apply/:id").get(function(req, res) { 112 | var id = req.params.id; 113 | token_db.collection("api_links").find({ id: id }, function(err, result) { 114 | if (result.length == 1) { 115 | token_db.collection("api_links").remove({ id: id }, function(e, d) { 116 | token_db 117 | .collection("api_token") 118 | .update( 119 | { token: result[0].token }, 120 | { $set: { active: true } }, 121 | function(e, d) { 122 | var data = { 123 | year: year, 124 | javascript_file: "token.min.js", 125 | captcha: res.recaptcha, 126 | analytics: analytics, 127 | adsense: adsense, 128 | adds: adds, 129 | activated: true, 130 | type: "website", 131 | token: result[0].token, 132 | correct: true, 133 | stylesheet: "style.css", 134 | embed: false, 135 | og_image: "https://zoff.me/assets/images/small-square.jpg" 136 | }; 137 | res.render("layouts/client/token", data); 138 | } 139 | ); 140 | }); 141 | } else { 142 | var data = { 143 | year: year, 144 | javascript_file: "token.min.js", 145 | captcha: res.recaptcha, 146 | analytics: analytics, 147 | adsense: adsense, 148 | adds: adds, 149 | activated: false, 150 | token: "", 151 | type: "website", 152 | correct: false, 153 | stylesheet: "style.css", 154 | embed: false, 155 | og_image: "https://zoff.me/assets/images/small-square.jpg" 156 | }; 157 | res.render("layouts/client/token", data); 158 | } 159 | }); 160 | }); 161 | 162 | function root(req, res, next) { 163 | try { 164 | var url = req.headers["x-forwarded-host"] 165 | ? req.headers["x-forwarded-host"] 166 | : req.headers.host.split(":")[0]; 167 | var subdomain = req.headers["x-forwarded-host"] 168 | ? req.headers["x-forwarded-host"].split(".") 169 | : req.headers.host.split(":")[0].split("."); 170 | if (subdomain[0] == "remote") { 171 | var data = { 172 | year: year, 173 | javascript_file: "remote.min.js", 174 | captcha: res.recaptcha, 175 | adsense: adsense, 176 | adds: adds, 177 | analytics: analytics, 178 | type: "website", 179 | stylesheet: "style.css", 180 | embed: false, 181 | client: false, 182 | og_image: "https://zoff.me/assets/images/small-square.jpg" 183 | }; 184 | res.render("layouts/client/remote", data); 185 | } else if (subdomain[0] == "www") { 186 | res.redirect("https://zoff.me"); 187 | } else { 188 | var data = { 189 | year: year, 190 | javascript_file: "main.min.js", 191 | captcha: res.recaptcha, 192 | adsense: adsense, 193 | adds: adds, 194 | analytics: analytics, 195 | stylesheet: "style.css", 196 | type: "website", 197 | embed: false, 198 | client: false, 199 | og_image: "https://zoff.me/assets/images/small-square.jpg", 200 | channels: [] 201 | }; 202 | if (subdomain[0] == "client") { 203 | data.client = true; 204 | } 205 | Frontpage.get_frontpage_lists(function(err, docs) { 206 | db.collection("connected_users").find({ _id: "total_users" }, function( 207 | err, 208 | tot 209 | ) { 210 | if (docs.length > 0) { 211 | data.channels_exist = true; 212 | data.channels = docs.slice(0, 12); 213 | data.channel_list = JSON.stringify(docs); 214 | } else { 215 | data.channels_exist = false; 216 | data.channels = []; 217 | data.channel_list = []; 218 | } 219 | data.viewers = tot[0].total_users.length; 220 | res.render("layouts/client/frontpage", data); 221 | }); 222 | }); 223 | } 224 | } catch (e) { 225 | console.log(e); 226 | } 227 | } 228 | 229 | function channel(req, res, next) { 230 | try { 231 | var url = req.headers["x-forwarded-host"] 232 | ? req.headers["x-forwarded-host"] 233 | : req.headers.host.split(":")[0]; 234 | var subdomain = req.headers["x-forwarded-host"] 235 | ? req.headers["x-forwarded-host"].split(".") 236 | : req.headers.host.split(":")[0].split("."); 237 | if (subdomain[0] == "remote") { 238 | var data = { 239 | year: year, 240 | javascript_file: "remote.min.js", 241 | captcha: res.recaptcha, 242 | adsense: adsense, 243 | adds: adds, 244 | analytics: analytics, 245 | type: "website", 246 | stylesheet: "style.css", 247 | embed: false, 248 | client: false, 249 | og_image: "https://zoff.me/assets/images/small-square.jpg" 250 | }; 251 | res.render("layouts/client/remote", data); 252 | } else if (subdomain.length >= 2 && subdomain[0] == "www") { 253 | res.redirect("https://zoff.me"); 254 | } else { 255 | if (req.params.channel_name == "o_callback") { 256 | res.redirect("/api/oauth"); 257 | } else { 258 | var data = { 259 | title: "404: File Not Found", 260 | list_name: capitalizeFirstLetter(req.params.channel_name), 261 | year: year, 262 | javascript_file: "main.min.js", 263 | captcha: res.recaptcha, 264 | adsense: adsense, 265 | adds: adds, 266 | analytics: analytics, 267 | type: "video", 268 | stylesheet: "style.css", 269 | embed: false, 270 | client: false, 271 | og_image: "https://zoff.me/assets/images/small-square.jpg" 272 | }; 273 | if (subdomain[0] == "client") { 274 | data.client = true; 275 | } 276 | res.render("layouts/client/channel", data); 277 | } 278 | } 279 | } catch (e) { 280 | res.redirect("https://zoff.me"); 281 | } 282 | } 283 | 284 | function capitalizeFirstLetter(string) { 285 | return string.charAt(0).toUpperCase() + string.slice(1); 286 | } 287 | 288 | module.exports = router; 289 | --------------------------------------------------------------------------------