Compare commits
38 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0950d34c84 | ||
|
|
649b395fad | ||
|
|
5c104f555a | ||
|
|
ad011c5ece | ||
|
|
0157262130 | ||
|
|
bec209c665 | ||
|
|
00ac19c80c | ||
|
|
2acba1a605 | ||
|
|
9ba95a073c | ||
|
|
02dc12e646 | ||
|
|
9057ed2899 | ||
|
|
46032a33fa | ||
|
|
1cdf5f8ab9 | ||
|
|
05f0eb0048 | ||
|
|
74a555b5f5 | ||
|
|
c5b000a30a | ||
|
|
2227577138 | ||
|
|
cf59498476 | ||
|
|
8b82c1d9c6 | ||
|
|
4dcbf09b68 | ||
|
|
a6f082d5f1 | ||
|
|
88b98993d2 | ||
|
|
54052d6cfc | ||
|
|
113cf7900e | ||
|
|
15e5513982 | ||
|
|
adab2eb3f9 | ||
|
|
85fbe6bb5a | ||
|
|
c9fb5b0b0e | ||
|
|
cb4c99727f | ||
|
|
fcb562397a | ||
|
|
46bcb040f2 | ||
|
|
a048e2094c | ||
|
|
2fc8f16e9d | ||
|
|
1da7cab9cd | ||
|
|
3f653c4893 | ||
|
|
5535917f08 | ||
|
|
e171415b30 | ||
|
|
f3306f2263 |
45
LICENSE
45
LICENSE
|
|
@ -1282,48 +1282,6 @@ copy of the Program in return for a fee.
|
|||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
-------Original Cytube License-------
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013-2021 Calvin Montgomery and contributors
|
||||
|
|
@ -1335,3 +1293,6 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
--------other assets--------
|
||||
thunder0.ogg - NOT CREATED FOR FOREST - https://www.youtube.com/watch?v=T-BOPr7NXME
|
||||
thunder1.ogg - NOT CREATED FOR FOREST - https://www.youtube.com/watch?v=tPs2m9_7cls
|
||||
|
|
|
|||
194
README.md
194
README.md
|
|
@ -1,4 +1,4 @@
|
|||
fore.st - Pineapple Express(v1.1-indev)
|
||||
fore.st - Pineapple Express += 3 (v1.1.3)
|
||||
======
|
||||
|
||||
fore.st is the server software for ourfore.st, a community based chat & synced video embedding site tailored to service
|
||||
|
|
@ -24,194 +24,8 @@ You can reach out by bugging rainbownapkin on the ttn discord or ourfore.st, you
|
|||
- Thanks to calzoneman for making [cytube](https://github.com/calzoneman/sync), that saved our asses.
|
||||
- Thanks to the core TTN community and everyone else who's ever used it, I was only there for the last handful of years but it was an absolute fuckin' ride. You guys are the best, it isn't TTN but I hope this at least help fills the gap.
|
||||
|
||||
## Pineapple Express Indev Release Notes
|
||||
This is the first indev release for fore.st 1.1 Pineapple Express. This is the last push before the codebase is merged upstream with the newest version of cytube. Should probably get that done sooner than later. Heres a check list of the planned/completed features in this revision:
|
||||
|
||||
dev goals for 1.1 pineapple express:
|
||||
|
||||
- quick shit & bugfixes ✓
|
||||
- change markdown filters to require three symbols on each side, quickest fix for filters ✓
|
||||
- move refresh button to title bar ✓
|
||||
- delete custom embed button, replace with button under generel "add video" button ✓
|
||||
- move playlist item count and length next to buttons & compact ui ✓
|
||||
- hide playlist frame and control from users who don't have permission to view playlist (no one can see the afterparty playlist except mods, why should they have the controls cluttering things up?) ✓
|
||||
- add "mention" to userlist dropdown menu ✓
|
||||
- add "toke with" to userlist dropdown menu for shits n gigs ✓
|
||||
- unlatch sync on pause and scrub, show sync button on titlebar when sync is unlatched, this wont support classic yt at the moment. Invidious support planned ✓
|
||||
- caption support for raw video (no saving captions in channel history just yet, this will be in the next update which will involve a rework/addition to the database) ✓
|
||||
- fix bugged airdate after "queue next" ✓
|
||||
- collapsing playlist items hides pref and airtime by default. Airdate on same line as title, airtime and pref below ✓
|
||||
- collapse/expand all playlist item button ✓
|
||||
- close playlist button ✓
|
||||
- basic ui fixes/tweaks ✓
|
||||
- disabled !toke link embedding on emote alt text ✓
|
||||
- fix chatbar resizing on new message notification (might just by cytube+) ✓
|
||||
- relicense to agpl ✓
|
||||
- disable/remove unregistered channels ✓
|
||||
- fix fucked up bottom border on chat/video ✓
|
||||
- rename "legacy playlist buttons" to compact, fix location. This seems like it could be usable if not better than default ✓
|
||||
- get res/aspect ratio player.js ✓
|
||||
- base.coffee ✓
|
||||
- videojs.coffee(hls,videojs,raw-file,gdrive,rtmp) ✓
|
||||
- dailymotion ~(this is kinda broken, likely not possible client-side without breaking CORS policy. This can wait until next version when an installation script including nginx and cors-proxy config gets added to support the catbox.moe image upload button which faces the same issue.
|
||||
- vimeo ✓
|
||||
- add player.js updates to twitch
|
||||
- save temporary vids to channel library
|
||||
|
||||
- slide out panel (not an end user feature in and of itself, however a common UI element used for most menus, made to be quick and ezpz ✓
|
||||
- function for opening, closing ✓
|
||||
- allow switching menus on panel without having to open/close it ✓
|
||||
- pretty slide out animation ✓
|
||||
|
||||
- improved poll UI ✓
|
||||
- poll panel auto-opens when poll starts ✓
|
||||
- button slides in chatbar from left, slides back after poll done ✓
|
||||
- on click toggles poll ui on left of chat menu ✓
|
||||
- improved poll chat announcements ~Original announcement removed. May implement once server whisper implemented.
|
||||
|
||||
- improved emote ui ✓
|
||||
- slide out emote ui ✓
|
||||
- search bar ✓
|
||||
- optional alphabet sort(default) ✓
|
||||
- optional legacy emote menu available ✓
|
||||
|
||||
- cytube+ ripout ✓
|
||||
- scroll to current item button ✓
|
||||
- orientation buttons in title bar ✓
|
||||
- quick settings(icon next to emote button) ✓
|
||||
- general pref
|
||||
- theme ✓
|
||||
- playback pref ✓
|
||||
- video orientation ✓
|
||||
- toggle orientation buttons ✓
|
||||
- sync threshold (in seconds) ✓
|
||||
- youtube source (add after degoogling) ~
|
||||
- chat pref ✓
|
||||
- use legacy cytube emote menu ✓
|
||||
- blink title on chat ✓
|
||||
- chat notification sound ✓
|
||||
- chat desktop notification ✓
|
||||
- show timestamps ✓
|
||||
- show seconds ✓
|
||||
|
||||
- trim/tidy default cytube command ✓
|
||||
- replace / with ! as defualt server-side command indicator to match tokebot and TTN commands. / will be used for future client-side commands ✓
|
||||
- me ✓
|
||||
- sp ✓
|
||||
- afk ✓
|
||||
- poll ✓
|
||||
- hpoll ✓
|
||||
- mute ✓
|
||||
- smute ✓
|
||||
- unmute ✓
|
||||
- kick ✓
|
||||
- ban ✓
|
||||
- ipban ✓
|
||||
- clear ✓
|
||||
- clean ✓
|
||||
- cleantitle ✓
|
||||
- remove/consolidate/add to commands ✓
|
||||
- say -> announce(change tokebot modflair CSS to announce CSS, make normal modflair for bot/admin rank) ✓
|
||||
- modflair on announce ✓
|
||||
- remove kickanons ✓
|
||||
- remove d (drink) ✓
|
||||
- add user specific function to clear ('!clear <username>' to clear chats by said user) ✓
|
||||
|
||||
- Merge Upstream to newest cytube commit ✓
|
||||
|
||||
- improved server-whisper system ✓
|
||||
- public leave/join messages ✓
|
||||
- server-whisper target parameter for user specific whispers ✓
|
||||
- server-whisper name ✓
|
||||
|
||||
- getplaylistlinks outputs in fpanel
|
||||
- I mean its pretty fucking simple I dont know how you can screw this one up bud.
|
||||
- ezpzlmnsqze
|
||||
|
||||
- flex layout/legacy layout ripout ✓
|
||||
- rip out legacy layout system ✓
|
||||
- chat/player sized to fit canvas with navbar (remove -+ buttons on player) ✓
|
||||
- theatre mode toggle icon on title bar (hides navbar, player/chat takes up entire screen) ✓
|
||||
- click to drag chat/player split ✓
|
||||
- lock videowrap width to aspect ratio ✓
|
||||
- lock on join ✓
|
||||
- unlock on split drag ✓
|
||||
- show lock button on unlock ✓
|
||||
- hide lock button when locked ✓
|
||||
- checklock/move split on window move ✓
|
||||
- checklock/move split on video load ✓
|
||||
- videojs.coffee(hls,videojs,raw-file,gdrive,rtmp) ✓
|
||||
- dailymotion ✓
|
||||
- vimeo ✓
|
||||
- shade player ✓
|
||||
- player controls in title bar when video shaded (play/pause, mute, current time/video duration), hidden when video open ✓
|
||||
- minicont update in videojs ✓
|
||||
- minicont update in dailymotion ✓
|
||||
- minicont update in vimeo ✓
|
||||
- shade chat (hide chat box, message box, send button. chat header bar collapses, retains user count and expansion arrows. Userlist/poll open/close is independent.) ✓
|
||||
- scroll over mincont dir to scrub vid ✓
|
||||
- scroll over minicont mutebtn to change vol ✓
|
||||
- remove legacy cytube themes. If they wheren't compatible after the emote panel, they certainly won't be now lmao ✓
|
||||
- fix bugs created by flex layout ✓
|
||||
- fix fpanel width ✓
|
||||
- fix portrait/mobile mode ✓
|
||||
- fix user tooltip menu position ✓
|
||||
|
||||
- decaffeinate player.js
|
||||
- Coffee script was a bad idea then, and it makes even less sense now.
|
||||
- fuck me its just javascript with worse syntax, no real debugging tools, and build times
|
||||
- literally fucking why though?(I guess it made sense before ES6 but still fuck me)
|
||||
|
||||
- degoogling
|
||||
- yt-dlp backend for serverside metadata acquisition of youtube videos w/o registered API key or google account
|
||||
- potentially leverage yt-dlp backend for other media sources
|
||||
- implement player.js updates into youtube.coffee
|
||||
- latching
|
||||
- getres
|
||||
- update minicont dur
|
||||
- update minicont buttons
|
||||
- invidious embed support for youtube video playback
|
||||
- youtube source in user prefrences (three or four invidious instances from different countries/continents, official youtube embed, or custom invidious instance)
|
||||
|
||||
- autobump
|
||||
- sepearate bump lists, based on js/txt files at first, will be stored in db next update (may use multiple at once)
|
||||
- skip next bump/disable bumping
|
||||
- bump frequency (default: 1)
|
||||
- queue method: random from last-half, round-robin, full random
|
||||
- override next bump
|
||||
- require video be at least 4 minutes to add bump (mods can override from bump menu)
|
||||
|
||||
- merge tokebot into ourfore.st codebase, one server instead of two.
|
||||
- profile and userlist entry
|
||||
- bot specific rank
|
||||
- reset cooldown accessible from modmenu (quiet and loud, quiet by default)
|
||||
- log tokes w/ date to file. This will be switched to mariadb in the next update
|
||||
- tokefile, list of usernames with toke count. This will be switched to mariadb in the next update
|
||||
- total tokes listed on profile tooltip
|
||||
|
||||
|
||||
- mod panel
|
||||
- button on chatbar
|
||||
- mod message (sends message to all active mods)
|
||||
- new poll button
|
||||
- autobump control tab
|
||||
- tokebot control tab
|
||||
- playlist tab
|
||||
- modflair
|
||||
- open playlist below video (if closed, otherwise this does not appear)
|
||||
- open playlist below video by default
|
||||
|
||||
|
||||
- merge fore.st theme changes to fore.st dusk, consider moving some of them over to cytube.css for easier management
|
||||
|
||||
- extra shit(probs wait til next update, or hotfix)
|
||||
- short chats (acronyms, emoji, single letters/numbers/symbols) pop in over video from left starting at top left, overflow pops in below, instead of in chat box. Chats slide back up into top of vid after 2s. (optional, default on)
|
||||
- basic mod chat (save to mod channel + pm all online mods)
|
||||
- basic profile page (in side panel)
|
||||
- css variables in theme for ez customizablity
|
||||
- user themes
|
||||
- change background to other themes background or img from url(theme background by default)
|
||||
- native odysee support (no raw embed)
|
||||
## Pineapple Express += 3 1.1.3 Release Notes
|
||||
- Add bulk-queueing from Internet Archive
|
||||
|
||||
## License
|
||||
Original fore.st code is provided under the Affero General Public License v3 in order to prevent fore.st being used in proprietary software.
|
||||
|
|
@ -220,5 +34,5 @@ Original fore.st code is provided under the Affero General Public License v3 in
|
|||
Cytube source code originally licensed under MIT license
|
||||
(see the LICENSE file for the full text.)
|
||||
|
||||
Bundled source code, such as third-party CSS and JavaScript libraries, are
|
||||
Bundled source code and assets, such as third-party Images, Audio, Video, CSS and JavaScript libraries, are
|
||||
provided under their respective licenses.
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var coffee = require('coffeescript');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
var order = [
|
||||
'base.coffee',
|
||||
'vimeo.coffee',
|
||||
'youtube.coffee',
|
||||
'dailymotion.coffee',
|
||||
'videojs.coffee',
|
||||
'playerjs.coffee',
|
||||
'streamable.coffee',
|
||||
'gdrive-player.coffee',
|
||||
'raw-file.coffee',
|
||||
'soundcloud.coffee',
|
||||
'embed.coffee',
|
||||
'twitch.coffee',
|
||||
'livestream.com.coffee',
|
||||
'custom-embed.coffee',
|
||||
'rtmp.coffee',
|
||||
'smashcast.coffee',
|
||||
'ustream.coffee',
|
||||
'imgur.coffee',
|
||||
'hls.coffee',
|
||||
'twitchclip.coffee',
|
||||
'update.coffee'
|
||||
];
|
||||
|
||||
var buffer = '';
|
||||
order.forEach(function (file) {
|
||||
buffer += fs.readFileSync(
|
||||
path.join(__dirname, '..', 'player', file)
|
||||
) + '\n';
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '..', 'www', 'js', 'player.js'),
|
||||
coffee.compile(buffer)
|
||||
);
|
||||
|
|
@ -112,6 +112,12 @@ io:
|
|||
# https.domain are included implicitly).
|
||||
allowed-origins: []
|
||||
|
||||
#pull info from invidious
|
||||
invidious-backend: true
|
||||
yt-dlp-path: 'yt-dlp'
|
||||
#invidious source, defaults to vid.puffyan.us, not affiliated, simply a well known US based instance
|
||||
#invidious-source: 'vid.puffyan.us'
|
||||
|
||||
# YouTube v3 API key
|
||||
# 1. Go to https://console.developers.google.com/, create a new "project" (or choose an existing one)
|
||||
# 2. Make sure the YouTube Data v3 API is "enabled" for your project: https://console.developers.google.com/apis/library/youtube.googleapis.com
|
||||
|
|
|
|||
10754
package-lock.json
generated
10754
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"author": "Calvin Montgomery",
|
||||
"name": "CyTube",
|
||||
"description": "Online media synchronizer and chat",
|
||||
"version": "3.82.11",
|
||||
"repository": {
|
||||
"url": "http://github.com/calzoneman/sync"
|
||||
"name": "fore.st",
|
||||
"version": "1.1.3",
|
||||
"description": "fore.st: A fork of cytube tailored for the TTN Community",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"lib": "lib"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@calzoneman/jsli": "^2.0.1",
|
||||
"@cytube/mediaquery": "github:CyTube/mediaquery#4f803961d72a4fc7a1e09c0babaf8ea685013b1b",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"express": "^4.17.1",
|
||||
"express-minify": "^1.0.0",
|
||||
"json-typecheck": "^0.1.3",
|
||||
"knex": "^0.95.2",
|
||||
"knex": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql": "^2.18.1",
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
"source-map-support": "^0.5.19",
|
||||
"toml": "^3.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yamljs": "^0.2.8"
|
||||
"yamljs": "^0.2.8",
|
||||
"youtube-dl-exec": "^3.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build-player": "./bin/build-player.js",
|
||||
"build-server": "babel -D --source-maps --out-dir lib/ src/",
|
||||
"flow": "flow",
|
||||
"lint": "eslint src",
|
||||
|
|
|
|||
5
patchnotes.md
Normal file
5
patchnotes.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
dev goals for 1.1.3 pineapple Express += 3:
|
||||
-- Add bulk-queueing from Internet Archive
|
||||
- automagically pull most web-compatible version of all videos within a specific upload to IA
|
||||
- allow queueing admin to filter directory by video length in minutes
|
||||
- allow for bulk naming
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
window.Player = class Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof Player)
|
||||
return new Player(data)
|
||||
|
||||
@setMediaProperties(data)
|
||||
@paused = false
|
||||
@latched = true
|
||||
@seeklatch = false #used to lock sync latch when seeking for sync
|
||||
@lastSTime = 0
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
|
||||
setMediaProperties: (data) ->
|
||||
@mediaId = data.id
|
||||
@mediaType = data.type
|
||||
@mediaLength = data.seconds
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
latch: ->
|
||||
if not @latched
|
||||
@latched = true
|
||||
unlatch: ->
|
||||
if not @seeklatch
|
||||
if @latched
|
||||
$("#latchvid").show()
|
||||
@latched = false
|
||||
else
|
||||
@seeklatch = false
|
||||
latchseek: ->
|
||||
@seeklatch = true
|
||||
getLatch: (cb) ->
|
||||
cb(@latched)
|
||||
seekTo: (time) ->
|
||||
|
||||
setVolume: (volume) ->
|
||||
|
||||
getTime: (cb) ->
|
||||
cb(0)
|
||||
|
||||
isPaused: (cb) ->
|
||||
cb(@paused)
|
||||
|
||||
getVolume: (cb) ->
|
||||
cb(VOLUME)
|
||||
|
||||
getRes: (cb) ->
|
||||
cb();
|
||||
|
||||
destroy: ->
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
CUSTOM_EMBED_WARNING = 'This channel is embedding custom content from %link%.
|
||||
Since this content is not trusted, you must click "Embed" below to allow
|
||||
the content to be embedded.<hr>'
|
||||
|
||||
window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof CustomEmbedPlayer)
|
||||
return new CustomEmbedPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
if not data.meta.embed?
|
||||
console.error('CustomEmbedPlayer::load(): missing meta.embed')
|
||||
return
|
||||
|
||||
embedSrc = data.meta.embed.src
|
||||
link = "<a href=\"#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>"
|
||||
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
|
||||
'alert-warning')
|
||||
.removeClass('col-md-12')
|
||||
$('<button/>').addClass('btn btn-default')
|
||||
.text('Embed')
|
||||
.click(=>
|
||||
super(data)
|
||||
)
|
||||
.appendTo(alert.find('.alert'))
|
||||
removeOld(alert)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
window.DailymotionPlayer = class DailymotionPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof DailymotionPlayer)
|
||||
return new DailymotionPlayer(data)
|
||||
|
||||
@setMediaProperties(data)
|
||||
@initialVolumeSet = false
|
||||
@playbackReadyCb = null
|
||||
@latched = true
|
||||
|
||||
waitUntilDefined(window, 'DM', =>
|
||||
removeOld()
|
||||
|
||||
params =
|
||||
autoplay: 1
|
||||
wmode: if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
||||
logo: 0
|
||||
|
||||
quality = @mapQuality(USEROPTS.default_quality)
|
||||
if quality != 'auto'
|
||||
params.quality = quality
|
||||
|
||||
@dm = DM.player('ytapiplayer',
|
||||
video: data.id
|
||||
width: parseInt(VWIDTH, 10)
|
||||
height: parseInt(VHEIGHT, 10)
|
||||
params: params
|
||||
)
|
||||
|
||||
@dm.addEventListener('apiready', =>
|
||||
@dmReady = true
|
||||
@dm.addEventListener('ended', ->
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
|
||||
@dm.addEventListener('pause', =>
|
||||
@paused = true
|
||||
setMini();
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
else
|
||||
@unlatch()
|
||||
)
|
||||
|
||||
@dm.addEventListener('playing', =>
|
||||
@paused = false
|
||||
setMini();
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
|
||||
if not @initialVolumeSet
|
||||
@setVolume(VOLUME)
|
||||
@initialVolumeSet = true
|
||||
)
|
||||
|
||||
@dm.addEventListener('seeked', =>
|
||||
if not CLIENT.leader
|
||||
@unlatch()
|
||||
)
|
||||
|
||||
@dm.addEventListener('timeupdate', =>
|
||||
setDur();
|
||||
)
|
||||
|
||||
@dm.addEventListener('volumechange', =>
|
||||
setMini();
|
||||
)
|
||||
|
||||
# Once the video stops, the internal state of the player
|
||||
# becomes unusable and attempting to load() will corrupt it and
|
||||
# crash the player with an error. As a short–medium term
|
||||
# workaround, mark the player as "not ready" until the next
|
||||
# playback_ready event
|
||||
@dm.addEventListener('video_end', =>
|
||||
@dmReady = false
|
||||
)
|
||||
@dm.addEventListener('playback_ready', =>
|
||||
@dmReady = true
|
||||
handleWindowResize()
|
||||
|
||||
if @playbackReadyCb
|
||||
@playbackReadyCb()
|
||||
@playbackReadyCb = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
if @dm and @dmReady
|
||||
@dm.load(data.id)
|
||||
@dm.seek(data.currentTime)
|
||||
|
||||
else if @dm
|
||||
# TODO: Player::load() needs to be made asynchronous in the future
|
||||
console.log('Warning: load() called before DM is ready, queueing callback')
|
||||
@playbackReadyCb = () =>
|
||||
handleWindowResize()
|
||||
|
||||
@dm.load(data.id)
|
||||
@dm.seek(data.currentTime)
|
||||
else
|
||||
console.error('WTF? DailymotionPlayer::load() called but @dm is undefined')
|
||||
|
||||
pause: ->
|
||||
if @dm and @dmReady
|
||||
@paused = true
|
||||
@dm.pause()
|
||||
|
||||
play: ->
|
||||
if @dm and @dmReady
|
||||
@paused = false
|
||||
@dm.play()
|
||||
|
||||
seekTo: (time) ->
|
||||
if @dm and @dmReady
|
||||
@dm.seek(time)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @dm and @dmReady
|
||||
@dm.setVolume(volume)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @dm and @dmReady
|
||||
cb(@dm.currentTime)
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @dm and @dmReady
|
||||
if @dm.muted
|
||||
cb(0)
|
||||
else
|
||||
volume = @dm.volume
|
||||
# There was once a bug in Dailymotion where it sometimes gave back
|
||||
# volumes in the wrong range. Not sure if this is still a necessary
|
||||
# check.
|
||||
if volume > 1
|
||||
volume /= 100
|
||||
cb(volume)
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
||||
getRes: (cb) ->
|
||||
if @dm and @dmReady
|
||||
cb([@dm.width, @dm.height])
|
||||
else
|
||||
cb()
|
||||
mapQuality: (quality) ->
|
||||
switch String(quality)
|
||||
when '240', '480', '720', '1080' then String(quality)
|
||||
when '360' then '380'
|
||||
when 'best' then '1080'
|
||||
else 'auto'
|
||||
|
||||
destroy: ->
|
||||
if @dm
|
||||
@dm.destroy('ytapiplayer')
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
DEFAULT_ERROR = 'You are currently connected via HTTPS but the embedded content
|
||||
uses non-secure plain HTTP. Your browser therefore blocks it from
|
||||
loading due to mixed content policy. To fix this, embed the video using a
|
||||
secure link if available (https://...), or find another source for the content.'
|
||||
|
||||
genParam = (name, value) ->
|
||||
$('<param/>').attr(
|
||||
name: name
|
||||
value: value
|
||||
)
|
||||
|
||||
window.EmbedPlayer = class EmbedPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof EmbedPlayer)
|
||||
return new EmbedPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
|
||||
embed = data.meta.embed
|
||||
if not embed?
|
||||
console.error('EmbedPlayer::load(): missing meta.embed')
|
||||
return
|
||||
|
||||
@player = @loadIframe(embed)
|
||||
|
||||
removeOld(@player)
|
||||
|
||||
loadIframe: (embed) ->
|
||||
if embed.src.indexOf('http:') == 0 and location.protocol == 'https:'
|
||||
if @__proto__.mixedContentError?
|
||||
error = @__proto__.mixedContentError
|
||||
else
|
||||
error = DEFAULT_ERROR
|
||||
alert = makeAlert('Mixed Content Error', error, 'alert-danger')
|
||||
.removeClass('col-md-12')
|
||||
alert.find('.close').remove()
|
||||
return alert
|
||||
else
|
||||
iframe = $('<iframe/>').attr(
|
||||
src: embed.src
|
||||
frameborder: '0'
|
||||
allow: 'autoplay'
|
||||
allowfullscreen: '1'
|
||||
)
|
||||
|
||||
return iframe
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof GoogleDrivePlayer)
|
||||
return new GoogleDrivePlayer(data)
|
||||
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
if not window.hasDriveUserscript
|
||||
window.promptToInstallDriveUserscript()
|
||||
else if window.hasDriveUserscript
|
||||
window.maybePromptToUpgradeUserscript()
|
||||
if typeof window.getGoogleDriveMetadata is 'function'
|
||||
setTimeout(=>
|
||||
backoffRetry((cb) ->
|
||||
window.getGoogleDriveMetadata(data.id, cb)
|
||||
, (error, metadata) =>
|
||||
if error
|
||||
console.error(error)
|
||||
alertBox = window.document.createElement('div')
|
||||
alertBox.className = 'alert alert-danger'
|
||||
alertBox.textContent = error
|
||||
document.getElementById('ytapiplayer').appendChild(alertBox)
|
||||
else
|
||||
data.meta.direct = metadata.videoMap
|
||||
super(data)
|
||||
, {
|
||||
maxTries: 3
|
||||
delay: 1000
|
||||
factor: 1.2
|
||||
jitter: 500
|
||||
})
|
||||
, Math.random() * 1000)
|
||||
|
||||
window.promptToInstallDriveUserscript = ->
|
||||
if document.getElementById('prompt-install-drive-userscript')
|
||||
return
|
||||
alertBox = document.createElement('div')
|
||||
alertBox.id = 'prompt-install-drive-userscript'
|
||||
alertBox.className = 'alert alert-info'
|
||||
alertBox.innerHTML = """
|
||||
Due to continual breaking changes making it increasingly difficult to
|
||||
maintain Google Drive support, Google Drive now requires installing
|
||||
a userscript in order to play the video."""
|
||||
alertBox.appendChild(document.createElement('br'))
|
||||
infoLink = document.createElement('a')
|
||||
infoLink.className = 'btn btn-info'
|
||||
infoLink.href = '/google_drive_userscript'
|
||||
infoLink.textContent = 'Click here for details'
|
||||
infoLink.target = '_blank'
|
||||
alertBox.appendChild(infoLink)
|
||||
|
||||
closeButton = document.createElement('button')
|
||||
closeButton.className = 'close pull-right'
|
||||
closeButton.innerHTML = '×'
|
||||
closeButton.onclick = ->
|
||||
alertBox.parentNode.removeChild(alertBox)
|
||||
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
||||
removeOld($('<div/>').append(alertBox))
|
||||
|
||||
window.tellUserNotToContactMeAboutThingsThatAreNotSupported = ->
|
||||
if document.getElementById('prompt-no-gdrive-support')
|
||||
return
|
||||
alertBox = document.createElement('div')
|
||||
alertBox.id = 'prompt-no-gdrive-support'
|
||||
alertBox.className = 'alert alert-danger'
|
||||
alertBox.innerHTML = """
|
||||
CyTube has detected an error in Google Drive playback. Please note that the
|
||||
staff in CyTube support channels DO NOT PROVIDE SUPPORT FOR GOOGLE DRIVE. It
|
||||
is left in the code as-is for existing users, but we will not assist in
|
||||
troubleshooting any errors that occur.<br>"""
|
||||
alertBox.appendChild(document.createElement('br'))
|
||||
infoLink = document.createElement('a')
|
||||
infoLink.className = 'btn btn-danger'
|
||||
infoLink.href = 'https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions#why-dont-you-support-google-drive-anymore'
|
||||
infoLink.textContent = 'Click here for details'
|
||||
infoLink.target = '_blank'
|
||||
alertBox.appendChild(infoLink)
|
||||
|
||||
closeButton = document.createElement('button')
|
||||
closeButton.className = 'close pull-right'
|
||||
closeButton.innerHTML = '×'
|
||||
closeButton.onclick = ->
|
||||
alertBox.parentNode.removeChild(alertBox)
|
||||
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
||||
removeOld($('<div/>').append(alertBox))
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
window.HLSPlayer = class HLSPlayer extends VideoJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof HLSPlayer)
|
||||
return new HLSPlayer(data)
|
||||
|
||||
@setupMeta(data)
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
@setupMeta(data)
|
||||
super(data)
|
||||
|
||||
setupMeta: (data) ->
|
||||
data.meta.direct =
|
||||
# Quality is required for data.meta.direct processing but doesn't
|
||||
# matter here because it's dictated by the stream. Arbitrarily
|
||||
# choose 480.
|
||||
480: [
|
||||
{
|
||||
link: data.id
|
||||
contentType: 'application/x-mpegURL'
|
||||
}
|
||||
]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
window.ImgurPlayer = class ImgurPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof ImgurPlayer)
|
||||
return new ImgurPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
tag: 'iframe'
|
||||
src: "https://imgur.com/a/#{data.id}/embed"
|
||||
super(data)
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
window.LivestreamPlayer = class LivestreamPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof LivestreamPlayer)
|
||||
return new LivestreamPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
src: "https://cdn.livestream.com/embed/#{data.id}?\
|
||||
layout=4&\
|
||||
color=0x000000&\
|
||||
iconColorOver=0xe7e7e7&\
|
||||
iconColor=0xcccccc"
|
||||
tag: 'iframe'
|
||||
super(data)
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
window.PlayerJSPlayer = class PlayerJSPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof PlayerJSPlayer)
|
||||
return new PlayerJSPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
@ready = false
|
||||
@finishing = false
|
||||
|
||||
if not data.meta.playerjs
|
||||
throw new Error('Invalid input: missing meta.playerjs')
|
||||
|
||||
waitUntilDefined(window, 'playerjs', =>
|
||||
iframe = $('<iframe/>')
|
||||
.attr(src: data.meta.playerjs.src)
|
||||
|
||||
removeOld(iframe)
|
||||
|
||||
@player = new playerjs.Player(iframe[0])
|
||||
@player.on('ready', =>
|
||||
@player.on('error', (error) =>
|
||||
console.error('PlayerJS error', error.stack)
|
||||
)
|
||||
@player.on('ended', ->
|
||||
# Streamable seems to not implement this since it loops
|
||||
# gotta use the timeupdate hack below
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
@player.on('timeupdate', (time) =>
|
||||
if time.duration - time.seconds < 1 and not @finishing
|
||||
setTimeout(=>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
@pause()
|
||||
, (time.duration - time.seconds) * 1000)
|
||||
@finishing = true
|
||||
)
|
||||
@player.on('play', ->
|
||||
@paused = false
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
@player.on('pause', ->
|
||||
@paused = true
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
|
||||
@player.setVolume(VOLUME * 100)
|
||||
|
||||
if not @paused
|
||||
@player.play()
|
||||
|
||||
@ready = true
|
||||
)
|
||||
)
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @player and @ready
|
||||
@player.play()
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @player and @ready
|
||||
@player.pause()
|
||||
|
||||
seekTo: (time) ->
|
||||
if @player and @ready
|
||||
@player.setCurrentTime(time)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @player and @ready
|
||||
@player.setVolume(volume * 100)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @player and @ready
|
||||
@player.getCurrentTime(cb)
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @player and @ready
|
||||
@player.getVolume((volume) ->
|
||||
cb(volume / 100)
|
||||
)
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
codecToMimeType = (codec) ->
|
||||
switch codec
|
||||
when 'mov/h264', 'mov/av1' then 'video/mp4'
|
||||
when 'flv/h264' then 'video/flv'
|
||||
when 'matroska/vp8', 'matroska/vp9', 'matroska/av1' then 'video/webm'
|
||||
when 'ogg/theora' then 'video/ogg'
|
||||
when 'mp3' then 'audio/mp3'
|
||||
when 'vorbis' then 'audio/ogg'
|
||||
when 'aac' then 'audio/aac'
|
||||
when 'opus' then 'audio/opus'
|
||||
else 'video/flv'
|
||||
|
||||
window.FilePlayer = class FilePlayer extends VideoJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof FilePlayer)
|
||||
return new FilePlayer(data)
|
||||
|
||||
data.meta.direct =
|
||||
480: [{
|
||||
contentType: codecToMimeType(data.meta.codec)
|
||||
link: data.id
|
||||
}]
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.direct =
|
||||
480: [{
|
||||
contentType: codecToMimeType(data.meta.codec)
|
||||
link: data.id
|
||||
}]
|
||||
super(data)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
window.RTMPPlayer = class RTMPPlayer extends VideoJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof RTMPPlayer)
|
||||
return new RTMPPlayer(data)
|
||||
|
||||
@setupMeta(data)
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
@setupMeta(data)
|
||||
super(data)
|
||||
|
||||
setupMeta: (data) ->
|
||||
data.meta.direct =
|
||||
# Quality is required for data.meta.direct processing but doesn't
|
||||
# matter here because it's dictated by the stream. Arbitrarily
|
||||
# choose 480.
|
||||
480: [
|
||||
{
|
||||
link: data.id
|
||||
contentType: 'rtmp/flv'
|
||||
}
|
||||
]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
window.SmashcastPlayer = class SmashcastPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof SmashcastPlayer)
|
||||
return new SmashcastPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
src: "https://www.smashcast.tv/embed/#{data.id}"
|
||||
tag: 'iframe'
|
||||
super(data)
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
window.SoundCloudPlayer = class SoundCloudPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof SoundCloudPlayer)
|
||||
return new SoundCloudPlayer(data)
|
||||
|
||||
@setMediaProperties(data)
|
||||
|
||||
waitUntilDefined(window, 'SC', =>
|
||||
removeOld()
|
||||
|
||||
# For tracks that are private, but embeddable, the API returns a
|
||||
# special URL to load into the player.
|
||||
# TODO: rename scuri?
|
||||
if data.meta.scuri
|
||||
soundUrl = data.meta.scuri
|
||||
else
|
||||
soundUrl = data.id
|
||||
|
||||
widget = $('<iframe/>').appendTo($('#ytapiplayer'))
|
||||
widget.attr(
|
||||
id: 'scplayer'
|
||||
src: "https://w.soundcloud.com/player/?url=#{soundUrl}"
|
||||
)
|
||||
|
||||
# Soundcloud embed widget doesn't have a volume control.
|
||||
sliderHolder = $('<div/>').attr('id', 'soundcloud-volume-holder')
|
||||
.insertAfter(widget)
|
||||
$('<span/>').attr('id', 'soundcloud-volume-label')
|
||||
.addClass('label label-default')
|
||||
.text('Volume')
|
||||
.appendTo(sliderHolder)
|
||||
volumeSlider = $('<div/>').attr('id', 'soundcloud-volume')
|
||||
.appendTo(sliderHolder)
|
||||
.slider(
|
||||
range: 'min'
|
||||
value: VOLUME * 100
|
||||
stop: (event, ui) =>
|
||||
@setVolume(ui.value / 100)
|
||||
)
|
||||
|
||||
@soundcloud = SC.Widget(widget[0])
|
||||
@soundcloud.bind(SC.Widget.Events.READY, =>
|
||||
@soundcloud.ready = true
|
||||
@setVolume(VOLUME)
|
||||
@play()
|
||||
|
||||
@soundcloud.bind(SC.Widget.Events.PAUSE, =>
|
||||
@paused = true
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
@soundcloud.bind(SC.Widget.Events.PLAY, =>
|
||||
@paused = false
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
@soundcloud.bind(SC.Widget.Events.FINISH, =>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
if @soundcloud and @soundcloud.ready
|
||||
if data.meta.scuri
|
||||
soundUrl = data.meta.scuri
|
||||
else
|
||||
soundUrl = data.id
|
||||
@soundcloud.load(soundUrl, auto_play: true)
|
||||
@soundcloud.bind(SC.Widget.Events.READY, =>
|
||||
@setVolume(VOLUME)
|
||||
)
|
||||
else
|
||||
console.error('SoundCloudPlayer::load() called but soundcloud is not ready')
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @soundcloud and @soundcloud.ready
|
||||
@soundcloud.play()
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @soundcloud and @soundcloud.ready
|
||||
@soundcloud.pause()
|
||||
|
||||
seekTo: (time) ->
|
||||
if @soundcloud and @soundcloud.ready
|
||||
# SoundCloud measures time in milliseconds while CyTube uses seconds.
|
||||
@soundcloud.seekTo(time * 1000)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @soundcloud and @soundcloud.ready
|
||||
@soundcloud.setVolume(volume * 100)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @soundcloud and @soundcloud.ready
|
||||
# Returned time is in milliseconds; CyTube expects seconds
|
||||
@soundcloud.getPosition((time) -> cb(time / 1000))
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @soundcloud and @soundcloud.ready
|
||||
@soundcloud.getVolume((vol) -> cb(vol / 100))
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
window.StreamablePlayer = class StreamablePlayer extends PlayerJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof StreamablePlayer)
|
||||
return new StreamablePlayer(data)
|
||||
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.playerjs =
|
||||
src: "https://streamable.com/e/#{data.id}"
|
||||
|
||||
super(data)
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
window.TWITCH_PARAMS_ERROR = 'The Twitch embed player now uses parameters which only
|
||||
work if the following requirements are met: (1) The embedding website uses
|
||||
HTTPS; (2) The embedding website uses the default port (443) and is accessed
|
||||
via https://example.com instead of https://example.com:port. I have no
|
||||
control over this -- see <a href="https://discuss.dev.twitch.tv/t/twitch-embedded-player-migration-timeline-update/25588" rel="noopener noreferrer" target="_blank">this Twitch post</a>
|
||||
for details'
|
||||
|
||||
window.TwitchPlayer = class TwitchPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof TwitchPlayer)
|
||||
return new TwitchPlayer(data)
|
||||
|
||||
@setMediaProperties(data)
|
||||
waitUntilDefined(window, 'Twitch', =>
|
||||
waitUntilDefined(Twitch, 'Player', =>
|
||||
@init(data)
|
||||
)
|
||||
)
|
||||
|
||||
init: (data) ->
|
||||
removeOld()
|
||||
|
||||
if location.hostname != location.host or location.protocol != 'https:'
|
||||
alert = makeAlert(
|
||||
'Twitch API Parameters',
|
||||
window.TWITCH_PARAMS_ERROR,
|
||||
'alert-danger'
|
||||
).removeClass('col-md-12')
|
||||
removeOld(alert)
|
||||
@twitch = null
|
||||
return
|
||||
|
||||
options =
|
||||
parent: [location.hostname]
|
||||
width: $('#ytapiplayer').width()
|
||||
height: $('#ytapiplayer').height()
|
||||
|
||||
if data.type is 'tv'
|
||||
# VOD
|
||||
options.video = data.id
|
||||
else
|
||||
# Livestream
|
||||
options.channel = data.id
|
||||
|
||||
@twitch = new Twitch.Player('ytapiplayer', options)
|
||||
@twitch.addEventListener(Twitch.Player.READY, =>
|
||||
@setVolume(VOLUME)
|
||||
@twitch.setQuality(@mapQuality(USEROPTS.default_quality))
|
||||
@twitch.addEventListener(Twitch.Player.PLAY, =>
|
||||
@paused = false
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
@twitch.addEventListener(Twitch.Player.PAUSE, =>
|
||||
@paused = true
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
@twitch.addEventListener(Twitch.Player.ENDED, =>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
try
|
||||
if data.type is 'tv'
|
||||
# VOD
|
||||
@twitch.setVideo(data.id)
|
||||
else
|
||||
# Livestream
|
||||
@twitch.setChannel(data.id)
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
pause: ->
|
||||
try
|
||||
@twitch.pause()
|
||||
@paused = true
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
play: ->
|
||||
try
|
||||
@twitch.play()
|
||||
@paused = false
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
seekTo: (time) ->
|
||||
try
|
||||
@twitch.seek(time)
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
getTime: (cb) ->
|
||||
try
|
||||
cb(@twitch.getCurrentTime())
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
setVolume: (volume) ->
|
||||
try
|
||||
@twitch.setVolume(volume)
|
||||
if volume > 0
|
||||
@twitch.setMuted(false)
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
getVolume: (cb) ->
|
||||
try
|
||||
if @twitch.isPaused()
|
||||
cb(0)
|
||||
else
|
||||
cb(@twitch.getVolume())
|
||||
catch error
|
||||
console.error(error)
|
||||
|
||||
mapQuality: (quality) ->
|
||||
switch String(quality)
|
||||
when '1080' then 'chunked'
|
||||
when '720' then 'high'
|
||||
when '480' then 'medium'
|
||||
when '360' then 'low'
|
||||
when '240' then 'mobile'
|
||||
when 'best' then 'chunked'
|
||||
else ''
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
window.TwitchClipPlayer = class TwitchClipPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof TwitchClipPlayer)
|
||||
return new TwitchClipPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
if location.hostname != location.host or location.protocol != 'https:'
|
||||
alert = makeAlert(
|
||||
'Twitch API Parameters',
|
||||
window.TWITCH_PARAMS_ERROR,
|
||||
'alert-danger'
|
||||
).removeClass('col-md-12')
|
||||
removeOld(alert)
|
||||
return
|
||||
|
||||
data.meta.embed =
|
||||
tag: 'iframe'
|
||||
src: "https://clips.twitch.tv/embed?clip=#{data.id}&parent=#{location.host}"
|
||||
super(data)
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
TYPE_MAP =
|
||||
yt: YouTubePlayer
|
||||
vi: VimeoPlayer
|
||||
dm: DailymotionPlayer
|
||||
gd: GoogleDrivePlayer
|
||||
gp: VideoJSPlayer
|
||||
fi: FilePlayer
|
||||
sc: SoundCloudPlayer
|
||||
li: LivestreamPlayer
|
||||
tw: TwitchPlayer
|
||||
tv: TwitchPlayer
|
||||
cu: CustomEmbedPlayer
|
||||
rt: RTMPPlayer
|
||||
hb: SmashcastPlayer
|
||||
us: UstreamPlayer
|
||||
im: ImgurPlayer
|
||||
hl: HLSPlayer
|
||||
sb: StreamablePlayer
|
||||
tc: TwitchClipPlayer
|
||||
cm: VideoJSPlayer
|
||||
|
||||
window.loadMediaPlayer = (data) ->
|
||||
try
|
||||
if window.PLAYER
|
||||
window.PLAYER.destroy()
|
||||
catch error
|
||||
console.error error
|
||||
|
||||
if data.meta.direct and data.type is 'vi'
|
||||
try
|
||||
window.PLAYER = new VideoJSPlayer(data)
|
||||
catch e
|
||||
console.error e
|
||||
else if data.type of TYPE_MAP
|
||||
try
|
||||
window.PLAYER = TYPE_MAP[data.type](data)
|
||||
catch e
|
||||
console.error e
|
||||
|
||||
window.handleMediaUpdate = (data) ->
|
||||
PLAYER = window.PLAYER
|
||||
|
||||
|
||||
#update airdate
|
||||
dispSTimes();
|
||||
|
||||
PLAYER.lastSTime = data.currentTime;
|
||||
if not PLAYER.latched #check if the shits latched, if not stop while we're ahead.
|
||||
return
|
||||
|
||||
# Do not update if the current time is past the end of the video, unless
|
||||
# the video has length 0 (which is a special case for livestreams)
|
||||
if (typeof PLAYER.mediaLength is 'number' and
|
||||
PLAYER.mediaLength > 0 and
|
||||
data.currentTime > PLAYER.mediaLength)
|
||||
return
|
||||
|
||||
# Negative currentTime indicates a lead-in for clients to load the video,
|
||||
# but not play it yet (helps with initial buffering)
|
||||
waiting = data.currentTime < 0
|
||||
|
||||
# Load a new video in the same player if the ID changed
|
||||
if data.id and data.id != PLAYER.mediaId
|
||||
if data.currentTime < 0
|
||||
data.currentTime = 0
|
||||
PLAYER.load(data)
|
||||
PLAYER.play()
|
||||
|
||||
if waiting
|
||||
PLAYER.seekTo(0)
|
||||
PLAYER.latchseek()
|
||||
# YouTube player has a race condition that crashes the player if
|
||||
# play(), seek(0), and pause() are called quickly without waiting
|
||||
# for events to fire. Setting a flag variable that is checked in the
|
||||
# event handler mitigates this.
|
||||
if PLAYER instanceof YouTubePlayer
|
||||
PLAYER.pauseSeekRaceCondition = true
|
||||
else
|
||||
PLAYER.pause()
|
||||
return
|
||||
else if PLAYER instanceof YouTubePlayer
|
||||
PLAYER.pauseSeekRaceCondition = false
|
||||
|
||||
if CLIENT.leader or not USEROPTS.synch
|
||||
return
|
||||
|
||||
if data.paused and not PLAYER.paused
|
||||
PLAYER.seekTo(data.currentTime)
|
||||
PLAYER.latchseek()
|
||||
PLAYER.pause()
|
||||
else if PLAYER.paused and not data.paused
|
||||
PLAYER.play()
|
||||
|
||||
PLAYER.getTime((seconds) ->
|
||||
time = data.currentTime
|
||||
diff = (time - seconds) or time
|
||||
accuracy = USEROPTS.sync_accuracy
|
||||
|
||||
# Dailymotion can't seek very accurately in Flash due to keyframe
|
||||
# placement. Accuracy should not be set lower than 5 or the video
|
||||
# may be very choppy.
|
||||
if PLAYER instanceof DailymotionPlayer
|
||||
accuracy = Math.max(accuracy, 5)
|
||||
|
||||
if diff > accuracy
|
||||
# The player is behind the correct time
|
||||
PLAYER.seekTo(time)
|
||||
PLAYER.latchseek()
|
||||
else if diff < -accuracy
|
||||
# The player is ahead of the correct time
|
||||
# Don't seek all the way back, to account for possible buffering.
|
||||
# However, do seek all the way back for Dailymotion due to the
|
||||
# keyframe issue mentioned above.
|
||||
if not (PLAYER instanceof DailymotionPlayer)
|
||||
time += 1
|
||||
PLAYER.seekTo(time)
|
||||
PLAYER.latchseek()
|
||||
)
|
||||
|
||||
window.removeOld = (replace) ->
|
||||
$('#soundcloud-volume-holder').remove()
|
||||
replace ?= $('<div/>').addClass('embed-responsive-item')
|
||||
old = $('#ytapiplayer')
|
||||
replace.insertBefore(old)
|
||||
old.remove()
|
||||
replace.attr('id', 'ytapiplayer')
|
||||
return replace
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
window.UstreamPlayer = class UstreamPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof UstreamPlayer)
|
||||
return new UstreamPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
tag: 'iframe'
|
||||
src: "https://www.ustream.tv/embed/#{data.id}?html5ui"
|
||||
super(data)
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
sortSources = (sources) ->
|
||||
if not sources
|
||||
console.error('sortSources() called with null source list')
|
||||
return []
|
||||
|
||||
qualities = ['2160', '1440', '1080', '720', '540', '480', '360', '240']
|
||||
pref = String(USEROPTS.default_quality)
|
||||
if USEROPTS.default_quality == 'best'
|
||||
pref = '2160'
|
||||
idx = qualities.indexOf(pref)
|
||||
if idx < 0
|
||||
idx = 5 # 480p
|
||||
|
||||
qualityOrder = qualities.slice(idx).concat(qualities.slice(0, idx).reverse())
|
||||
qualityOrder.unshift('auto')
|
||||
sourceOrder = []
|
||||
flvOrder = []
|
||||
for quality in qualityOrder
|
||||
if quality of sources
|
||||
flv = []
|
||||
nonflv = []
|
||||
sources[quality].forEach((source) ->
|
||||
source.quality = quality
|
||||
if source.contentType == 'video/flv'
|
||||
flv.push(source)
|
||||
else
|
||||
nonflv.push(source)
|
||||
)
|
||||
sourceOrder = sourceOrder.concat(nonflv)
|
||||
flvOrder = flvOrder.concat(flv)
|
||||
|
||||
return sourceOrder.concat(flvOrder).map((source) ->
|
||||
type: source.contentType
|
||||
src: source.link
|
||||
res: source.quality
|
||||
label: getSourceLabel(source)
|
||||
)
|
||||
|
||||
getSourceLabel = (source) ->
|
||||
if source.res is 'auto'
|
||||
return 'auto'
|
||||
else
|
||||
return "#{source.quality}p #{source.contentType.split('/')[1]}"
|
||||
|
||||
waitUntilDefined(window, 'videojs', =>
|
||||
videojs.options.flash.swf = '/video-js.swf'
|
||||
)
|
||||
|
||||
hasAnyTextTracks = (data) ->
|
||||
ntracks = data?.meta?.textTracks?.length ? 0
|
||||
return ntracks > 0
|
||||
|
||||
window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof VideoJSPlayer)
|
||||
return new VideoJSPlayer(data)
|
||||
@load(data)
|
||||
|
||||
loadPlayer: (data) ->
|
||||
waitUntilDefined(window, 'videojs', =>
|
||||
attrs =
|
||||
width: '100%'
|
||||
height: '100%'
|
||||
|
||||
if @mediaType == 'cm' and hasAnyTextTracks(data)
|
||||
attrs.crossorigin = 'anonymous'
|
||||
|
||||
video = $('<video/>')
|
||||
.addClass('video-js vjs-default-skin embed-responsive-item')
|
||||
.attr(attrs)
|
||||
removeOld(video)
|
||||
|
||||
@sources = sortSources(data.meta.direct)
|
||||
if @sources.length == 0
|
||||
console.error('VideoJSPlayer::constructor(): data.meta.direct
|
||||
has no sources!')
|
||||
@mediaType = null
|
||||
return
|
||||
|
||||
@sourceIdx = 0
|
||||
|
||||
# TODO: Refactor VideoJSPlayer to use a preLoad()/load()/postLoad() pattern
|
||||
# VideoJSPlayer should provide the core functionality and logic for specific
|
||||
# dependent player types (gdrive) should be an extension
|
||||
if data.meta.gdrive_subtitles
|
||||
data.meta.gdrive_subtitles.available.forEach((subt) ->
|
||||
label = subt.lang_original
|
||||
if subt.name
|
||||
label += " (#{subt.name})"
|
||||
$('<track/>').attr(
|
||||
src: "/gdvtt/#{data.id}/#{subt.lang}/#{subt.name}.vtt?\
|
||||
vid=#{data.meta.gdrive_subtitles.vid}"
|
||||
kind: 'subtitles'
|
||||
srclang: subt.lang
|
||||
label: label
|
||||
).appendTo(video)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@player = videojs(video[0],
|
||||
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
|
||||
autoplay: @sources[0].type != 'application/dash+xml',
|
||||
controls: true,
|
||||
plugins:
|
||||
videoJsResolutionSwitcher:
|
||||
default: @sources[0].res
|
||||
)
|
||||
@player.ready(=>
|
||||
# Have to use updateSrc instead of <source> tags
|
||||
# see: https://github.com/videojs/video.js/issues/3428
|
||||
@player.updateSrc(@sources)
|
||||
@player.on('error', =>
|
||||
err = @player.error()
|
||||
if err and err.code == 4
|
||||
console.error('Caught error, trying next source')
|
||||
# Does this really need to be done manually?
|
||||
@sourceIdx++
|
||||
if @sourceIdx < @sources.length
|
||||
@player.src(@sources[@sourceIdx])
|
||||
else
|
||||
console.error('Out of sources, video will not play')
|
||||
if @mediaType is 'gd'
|
||||
if not window.hasDriveUserscript
|
||||
window.promptToInstallDriveUserscript()
|
||||
else
|
||||
window.tellUserNotToContactMeAboutThingsThatAreNotSupported()
|
||||
)
|
||||
@setVolume(VOLUME)
|
||||
@player.on('ended', ->
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
|
||||
@player.on('pause', =>
|
||||
@paused = true
|
||||
setMini();
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
else
|
||||
@unlatch()
|
||||
|
||||
)
|
||||
|
||||
@player.on('play', =>
|
||||
@paused = false
|
||||
setMini();
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
|
||||
@player.on('volumechange', =>
|
||||
setMini();
|
||||
)
|
||||
|
||||
# Workaround for IE-- even after seeking completes, the loading
|
||||
# spinner remains.
|
||||
@player.on('seeked', =>
|
||||
$('.vjs-waiting').removeClass('vjs-waiting')
|
||||
if not CLIENT.leader #this part has nothing to do with IE and all to do with sync latching :P
|
||||
@unlatch()
|
||||
|
||||
)
|
||||
|
||||
|
||||
@player.on('canplay', =>
|
||||
handleWindowResize()
|
||||
console.log(@player.children)
|
||||
)
|
||||
|
||||
@player.on('timeupdate', =>
|
||||
setDur();
|
||||
)
|
||||
|
||||
# Workaround for Chrome-- it seems that the click bindings for
|
||||
# the subtitle menu aren't quite set up until after the ready
|
||||
# event finishes, so set a timeout for 1ms to force this code
|
||||
# not to run until the ready() function returns.
|
||||
setTimeout(->
|
||||
$('#ytapiplayer .vjs-subtitles-button .vjs-menu-item').each((i, elem) ->
|
||||
textNode = elem.childNodes[0]
|
||||
if textNode.textContent == localStorage.lastSubtitle
|
||||
elem.click()
|
||||
|
||||
elem.onclick = ->
|
||||
if elem.attributes['aria-checked'].value == 'true'
|
||||
localStorage.lastSubtitle = textNode.textContent
|
||||
)
|
||||
, 1)
|
||||
)
|
||||
)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
@latched = true
|
||||
# Note: VideoJS does have facilities for loading new videos into the
|
||||
# existing player object, however it appears to be pretty glitchy when
|
||||
# a video can't be played (either previous or next video). It's safer
|
||||
# to just reset the entire thing.
|
||||
@destroy()
|
||||
@loadPlayer(data)
|
||||
@setTracks(data)
|
||||
|
||||
setTracks: (data) ->
|
||||
if data.meta.textTracks
|
||||
data.meta.textTracks.forEach((track) ->
|
||||
label = track.name
|
||||
$('<track>').attr(
|
||||
src: track.url
|
||||
kind: 'subtitles'
|
||||
type: track.type
|
||||
label: label
|
||||
default: true
|
||||
).prependTo("video")
|
||||
)
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @player and @player.readyState() > 0
|
||||
@player.play()
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @player and @player.readyState() > 0
|
||||
@player.pause()
|
||||
if not CLIENT.leader
|
||||
@unlatch()
|
||||
|
||||
|
||||
seekTo: (time) ->
|
||||
if @player and @player.readyState() > 0
|
||||
@player.currentTime(time)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @player
|
||||
@player.volume(volume)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @player and @player.readyState() > 0
|
||||
cb(@player.currentTime())
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @player and @player.readyState() > 0
|
||||
if @player.muted()
|
||||
cb(0)
|
||||
else
|
||||
cb(@player.volume())
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
||||
getRes: (cb) ->
|
||||
if @player
|
||||
cb([@player.videoWidth(), @player.videoHeight()])
|
||||
else
|
||||
cb()
|
||||
|
||||
destroy: ->
|
||||
removeOld()
|
||||
if @player
|
||||
@player.dispose()
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
window.VimeoPlayer = class VimeoPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof VimeoPlayer)
|
||||
return new VimeoPlayer(data)
|
||||
|
||||
@load(data)
|
||||
@resx = 0
|
||||
@resy = 0
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
@latched = true
|
||||
|
||||
waitUntilDefined(window, 'Vimeo', =>
|
||||
video = $('<iframe/>')
|
||||
removeOld(video)
|
||||
video.attr(
|
||||
src: "https://player.vimeo.com/video/#{data.id}"
|
||||
webkitallowfullscreen: true
|
||||
mozallowfullscreen: true
|
||||
allowfullscreen: true
|
||||
)
|
||||
|
||||
if USEROPTS.wmode_transparent
|
||||
video.attr('wmode', 'transparent')
|
||||
|
||||
@vimeo = new Vimeo.Player(video[0])
|
||||
|
||||
@vimeo.on('ended', =>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
|
||||
@vimeo.on('pause', =>
|
||||
@paused = true
|
||||
setMini();
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
else
|
||||
@unlatch()
|
||||
|
||||
)
|
||||
|
||||
@vimeo.on('play', =>
|
||||
@paused = false
|
||||
setMini();
|
||||
handleWindowResize()
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
|
||||
@vimeo.on('seeked', =>
|
||||
if not CLIENT.leader
|
||||
@unlatch()
|
||||
)
|
||||
|
||||
@vimeo.on('timeupdate', =>
|
||||
setDur();
|
||||
)
|
||||
|
||||
@vimeo.on('volumechange', =>
|
||||
setMini();
|
||||
)
|
||||
|
||||
|
||||
@play()
|
||||
@setVolume(VOLUME)
|
||||
|
||||
@vimeo.getVideoWidth().then((wid) ->
|
||||
window.PLAYER.resx = wid
|
||||
).catch((error) ->
|
||||
console.error('vimeo::getVideoWidth():', error)
|
||||
)
|
||||
|
||||
@vimeo.getVideoHeight().then((hgt) ->
|
||||
window.PLAYER.resy = hgt
|
||||
).catch((error) ->
|
||||
console.error('vimeo::getVideoHeight():', error)
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @vimeo
|
||||
@vimeo.play().catch((error) ->
|
||||
console.error('vimeo::play():', error)
|
||||
)
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @vimeo
|
||||
@vimeo.pause().catch((error) ->
|
||||
console.error('vimeo::pause():', error)
|
||||
)
|
||||
|
||||
seekTo: (time) ->
|
||||
if @vimeo
|
||||
@vimeo.setCurrentTime(time).catch((error) ->
|
||||
console.error('vimeo::setCurrentTime():', error)
|
||||
)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @vimeo
|
||||
@vimeo.setVolume(volume).catch((error) ->
|
||||
console.error('vimeo::setVolume():', error)
|
||||
)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @vimeo
|
||||
@vimeo.getCurrentTime().then((time) ->
|
||||
cb(parseFloat(time))
|
||||
).catch((error) ->
|
||||
console.error('vimeo::getCurrentTime():', error)
|
||||
)
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @vimeo
|
||||
@vimeo.getVolume().then((volume) ->
|
||||
cb(parseFloat(volume))
|
||||
).catch((error) ->
|
||||
console.error('vimeo::getVolume():', error)
|
||||
)
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
||||
getRes: (cb) ->
|
||||
if @vimeo
|
||||
cb([@resx,@resy])
|
||||
else
|
||||
cb()
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
window.YouTubePlayer = class YouTubePlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof YouTubePlayer)
|
||||
return new YouTubePlayer(data)
|
||||
|
||||
@setMediaProperties(data)
|
||||
@pauseSeekRaceCondition = false
|
||||
|
||||
waitUntilDefined(window, 'YT', =>
|
||||
# Even after window.YT is defined, YT.Player may not be, which causes a
|
||||
# 'YT.Player is not a constructor' error occasionally
|
||||
waitUntilDefined(YT, 'Player', =>
|
||||
removeOld()
|
||||
|
||||
wmode = if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
||||
@yt = new YT.Player('ytapiplayer',
|
||||
videoId: data.id
|
||||
playerVars:
|
||||
autohide: 1
|
||||
autoplay: 1
|
||||
controls: 1
|
||||
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
|
||||
rel: 0
|
||||
wmode: wmode
|
||||
events:
|
||||
onReady: @onReady.bind(this)
|
||||
onStateChange: @onStateChange.bind(this)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
if @yt and @yt.ready
|
||||
@yt.loadVideoById(data.id, data.currentTime)
|
||||
else
|
||||
console.error('WTF? YouTubePlayer::load() called but yt is not ready')
|
||||
|
||||
onReady: ->
|
||||
@yt.ready = true
|
||||
@setVolume(VOLUME)
|
||||
|
||||
onStateChange: (ev) ->
|
||||
# If you pause the video before the first PLAYING
|
||||
# event is emitted, weird things happen (or at least that was true
|
||||
# whenever this comment was authored in 2015).
|
||||
if ev.data == YT.PlayerState.PLAYING and @pauseSeekRaceCondition
|
||||
@pause()
|
||||
@pauseSeekRaceCondition = false
|
||||
|
||||
if (ev.data == YT.PlayerState.PAUSED and not @paused) or
|
||||
(ev.data == YT.PlayerState.PLAYING and @paused)
|
||||
@paused = (ev.data == YT.PlayerState.PAUSED)
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
|
||||
if ev.data == YT.PlayerState.ENDED and CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @yt and @yt.ready
|
||||
@yt.playVideo()
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @yt and @yt.ready
|
||||
@yt.pauseVideo()
|
||||
|
||||
seekTo: (time) ->
|
||||
if @yt and @yt.ready
|
||||
@yt.seekTo(time, true)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @yt and @yt.ready
|
||||
if volume > 0
|
||||
# If the player is muted, even if the volume is set,
|
||||
# the player remains muted
|
||||
@yt.unMute()
|
||||
@yt.setVolume(volume * 100)
|
||||
|
||||
setQuality: (quality) ->
|
||||
# https://github.com/calzoneman/sync/issues/726
|
||||
|
||||
getTime: (cb) ->
|
||||
if @yt and @yt.ready
|
||||
cb(@yt.getCurrentTime())
|
||||
else
|
||||
cb(0)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @yt and @yt.ready
|
||||
if @yt.isMuted()
|
||||
cb(0)
|
||||
else
|
||||
cb(@yt.getVolume() / 100)
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
|
@ -9,6 +9,4 @@ fi
|
|||
|
||||
echo "Building from src/ to lib/"
|
||||
npm run build-server
|
||||
echo "Building from player/ to www/js/player.js"
|
||||
npm run build-player
|
||||
echo "Done"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ AnonymousCheck.prototype.onUserPreJoin = function (user, data, cb) {
|
|||
return cb("User disconnected", ChannelModule.DENY);
|
||||
}
|
||||
|
||||
if(anonymousBanned && user.isAnonymous()) {
|
||||
if(/*anonymousBanned &&*/ user.isAnonymous()) {//we're just dropping the config check for now, but anonymous users shouldnt be allowed
|
||||
user.socket.on("disconnect", function () {
|
||||
if (!user.is(Flags.U_IN_CHANNEL)) {
|
||||
cb("User disconnected", ChannelModule.DENY);
|
||||
|
|
@ -23,6 +23,8 @@ AnonymousCheck.prototype.onUserPreJoin = function (user, data, cb) {
|
|||
});
|
||||
|
||||
user.socket.emit("errorMsg", { msg : "Welcome to ourfore.st! Register to start chatting/streaming!"});
|
||||
user.socket.disconnect();
|
||||
|
||||
user.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
});
|
||||
|
|
|
|||
998
src/channel/autobump.js
Normal file
998
src/channel/autobump.js
Normal file
|
|
@ -0,0 +1,998 @@
|
|||
/*
|
||||
fore.st is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
fore.st is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with fore.st. If not, see < http://www.gnu.org/licenses/ >.
|
||||
(C) 2022- by rainbownapkin, <ourforest@420blaze.it>
|
||||
*/
|
||||
import fs from 'fs';
|
||||
|
||||
const InfoGetter = require ("../get-info");
|
||||
const XSS = require("../xss");
|
||||
const ChannelModule = require("./module");
|
||||
const Media = require("../media");
|
||||
const util = require("../utilities");
|
||||
const Server = require("../server");
|
||||
|
||||
//type declerations
|
||||
const TYPE_NEWBUMP = {
|
||||
name: "string",
|
||||
user: "string",
|
||||
id: "string",
|
||||
type: "string",
|
||||
rtoke: "boolean",
|
||||
bumplist: "string",
|
||||
noauto: "boolean"
|
||||
};
|
||||
|
||||
const TYPE_DELBUMP = {
|
||||
bl: "string",
|
||||
id: "number"
|
||||
}
|
||||
|
||||
const TYPE_SELTYPE = {
|
||||
bump: "number",
|
||||
list: "number"
|
||||
}
|
||||
|
||||
const TYPE_BFREQ = {
|
||||
min: "number",
|
||||
max: "number"
|
||||
}
|
||||
|
||||
const TYPE_RENAME = {
|
||||
bl: "string",
|
||||
id: "number",
|
||||
name: "string"
|
||||
}
|
||||
|
||||
const TYPE_LISTRENAME = {
|
||||
oldname: "string",
|
||||
newname: "string"
|
||||
}
|
||||
|
||||
//global vars
|
||||
var bumplists = null;//decalare variable but keep it null until lists are loaded
|
||||
var lowerReg = /[\s!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g;//regex for ripping out specials and whitespace from lowernames
|
||||
var bumpFolder = "bumps/"
|
||||
var configFolder = bumpFolder + "config/"
|
||||
|
||||
//global functions
|
||||
function randi(len) {//get random number from zero to len, meant for use to pull random items from an array
|
||||
return Math.floor(Math.random() * len); //The maximum is exclusive and the minimum is inclusive
|
||||
}
|
||||
|
||||
function randrange(min,max){
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is exclusive and the minimum is inclusive
|
||||
}
|
||||
|
||||
function loadList(bfile){
|
||||
var _this = this;
|
||||
fs.readFile(bfile, function(err,rdata){
|
||||
if(err){
|
||||
console.log("[loadlist] " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = JSON.parse(rdata);
|
||||
var blist = [];
|
||||
data.bumps.forEach(function(b){
|
||||
if(b != null){
|
||||
var nbump = new bump(b.name, b.user, b.rtoke, b.media, null, b.noauto);
|
||||
nbump.listname = data.lowername;
|
||||
nbump.id = b.id;
|
||||
blist[b.id] = nbump;
|
||||
}
|
||||
});
|
||||
|
||||
bumplists.set(data.lowername, new bumplist(data.name,blist));
|
||||
});
|
||||
}
|
||||
|
||||
function loadLists(cb, callp){
|
||||
fs.readdir(bumpFolder, function(err, item){
|
||||
if(err){
|
||||
console.log("[loadlists] " + err);
|
||||
}
|
||||
|
||||
bumplists = new Map();//create new map to load lists into, this clears the variable as well as lets the channel know whether or not they have been loaded yet.
|
||||
|
||||
if(item != null){
|
||||
item.forEach(function(list, i){
|
||||
if(list != configFolder.slice(bumpFolder.length, configFolder.length - 1) && list.slice(list.length - 5) === ".bump"){
|
||||
loadList("bumps/" + list);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(cb != null){//lil' nasty but it calls loadconfig after loading lists :P
|
||||
if(callp != null){
|
||||
cb(callp);
|
||||
}else{
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//constructors
|
||||
function bump(name, user, rtoke, media, bumplist, noauto){//bump object constructor
|
||||
this.name = name;
|
||||
this.user = user;
|
||||
this.lowername = name.toLowerCase().replace(lowerReg, "");
|
||||
this.rtoke = rtoke;
|
||||
this.id = null;//this is assigned by the bumplist :P
|
||||
this.media = media;
|
||||
this.noauto = noauto
|
||||
if(bumplist != null){
|
||||
if(bumplists.get(bumplist.toLowerCase().replace(lowerReg, ""))){//if bumplist exists
|
||||
bumplists.get(bumplist.toLowerCase().replace(lowerReg, "")).addBump(this);//add this to the bumplist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bumplist(name, bumps){//bumplist object constructor
|
||||
this.name = name;
|
||||
this.lowername = this.name.toLowerCase().replace(lowerReg, "");
|
||||
this.bumps = (bumps == null ? [] : bumps);
|
||||
}
|
||||
|
||||
//prototypes
|
||||
//bump
|
||||
bump.prototype.rename = function(name){
|
||||
this.name = name;
|
||||
this.lowername = this.name.toLowerCase().replace(lowerReg, "");
|
||||
}
|
||||
|
||||
//bumplist
|
||||
bumplist.prototype.saveList = function(oldname){
|
||||
var _this = this;
|
||||
fs.writeFile("bumps/" + this.lowername + ".bump", JSON.stringify(this), function(err,data){ //RIPPED FROM TOKEBOT, NOT CHANGED YET
|
||||
if(err){
|
||||
console.log("[bump] BUMP LIST " + _this.name + " FILE WRITE ERROR: " + err);
|
||||
}
|
||||
|
||||
Server.getServer().channels.forEach(function(channel){
|
||||
channel.users.forEach(function(user){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
channel.modules.autobump.sendList(user, _this.lowername, oldname);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
bumplist.prototype.addBump = function(bump){//add bump to bumplist
|
||||
bump.id = (this.getLastId());//Set ID to the ID of the last item + 1 unless there is no item
|
||||
bump.listname = this.lowername;
|
||||
|
||||
this.bumps[bump.id] = bump;//add bump at bump id
|
||||
this.saveList();
|
||||
};
|
||||
|
||||
bumplist.prototype.deleteBump = function(bump){//delete bumps (should probably adjust ids and index of bumps but meh)
|
||||
if(bump != null){
|
||||
this.bumps[bump.id] = null;//null out bump
|
||||
this.saveList();//save the bitch
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
bumplist.prototype.getLastId = function(){
|
||||
var lid = 0;
|
||||
this.bumps.forEach(function(bump){
|
||||
if(bump != null){
|
||||
lid = bump.id + 1;
|
||||
}
|
||||
});
|
||||
return lid;
|
||||
}
|
||||
|
||||
bumplist.prototype.packList = function(){
|
||||
var pbl = {
|
||||
name: this.name,
|
||||
lowername: this.lowername,
|
||||
bumps: []
|
||||
}
|
||||
|
||||
this.bumps.forEach(function(bump){
|
||||
if(bump != null){
|
||||
pbl.bumps[bump.id] = {
|
||||
name: bump.name,
|
||||
lowername: bump.lowername,
|
||||
rtoke: bump.rtoke,
|
||||
id: bump.id,
|
||||
noauto: bump.noauto,
|
||||
media: {
|
||||
id: bump.media.id,
|
||||
title: bump.media.title,
|
||||
type: bump.media.type,
|
||||
duration: bump.media.duration
|
||||
}
|
||||
}
|
||||
|
||||
if(bump.user != null){
|
||||
pbl.bumps[bump.id].user = bump.user;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pbl;
|
||||
}
|
||||
|
||||
bumplist.prototype.rename = function(oldname,nname,cb){
|
||||
var _this = this;
|
||||
var lnname = nname.toLowerCase().replace(lowerReg, "");
|
||||
|
||||
fs.rename("bumps/" + this.lowername + ".bump", "bumps/" + lnname + ".bump", function(err){
|
||||
if(err){
|
||||
console.log("[bump] BUMP LIST " + _this.name + " FILE RENAME ERROR: " + err);
|
||||
}
|
||||
|
||||
bumplists.delete(_this.lowername);
|
||||
|
||||
_this.name = nname;
|
||||
_this.lowername = lnname;
|
||||
|
||||
_this.bumps.forEach(function(bump){
|
||||
if(bump != null){
|
||||
bump.listname = _this.lowername;
|
||||
}
|
||||
});
|
||||
|
||||
bumplists.set(_this.lowername, _this);//create new bumplist
|
||||
|
||||
_this.saveList(oldname);
|
||||
if(typeof cb === "function"){
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bumplist.prototype.deleteList = function(){
|
||||
var _this = this;
|
||||
fs.unlink("bumps/" + this.lowername + ".bump", function (err){
|
||||
if(err){
|
||||
console.log("[bump] BUMP LIST " + _this.name + " FILE DELETE ERROR: " + err);
|
||||
}
|
||||
|
||||
Server.getServer().channels.forEach(function(channel){
|
||||
channel.users.forEach(function(user){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
user.socket.emit("rmBumplist",_this.lowername);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bumplists.delete(_this.lowername);
|
||||
});
|
||||
};
|
||||
|
||||
//constructor
|
||||
function AutobumpModule(_channel){
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.selmed = this.lastHalfRandom;
|
||||
this.listsel = this.smashList;
|
||||
if(bumplists == null){
|
||||
loadLists(this.loadConfig, this);//on startup load lists
|
||||
}else{
|
||||
this.loadConfig();
|
||||
}
|
||||
}
|
||||
|
||||
//module protoype definition
|
||||
AutobumpModule.prototype = Object.create(ChannelModule.prototype);
|
||||
AutobumpModule.prototype.lastPlayed = [];
|
||||
AutobumpModule.prototype.activeLists = new Map();
|
||||
|
||||
//event handling
|
||||
AutobumpModule.prototype.onUserPostJoin = function (user){//on user join
|
||||
if(user.account.effectiveRank >= 2){
|
||||
user.socket.typecheckedOn("newBump", TYPE_NEWBUMP, this.handleNewBump.bind(this, user));//handle newBump
|
||||
user.socket.typecheckedOn("deleteBump", TYPE_DELBUMP, this.handleDeleteBump.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("renameBump", TYPE_RENAME, this.handleRenameBump.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("changeCreator", TYPE_RENAME, this.handleChangeCreator.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("toggleRtoke", TYPE_DELBUMP, this.handleToggleRtoke.bind(this, user));//toggle rtoke
|
||||
user.socket.typecheckedOn("toggleNoauto", TYPE_DELBUMP, this.handleToggleNoauto.bind(this, user));//toggle rtoke
|
||||
user.socket.typecheckedOn("newBumplist", '', this.handleNewBumplist.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("cloneBumplist", '', this.handleCloneBumplist.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("delBumplist", '', this.handleDelBumplist.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("renameBumplist", TYPE_LISTRENAME, this.handleRenameBumplist.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("setActive", '', this.handleSetActive.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("removeActive", '', this.handleRemoveActive.bind(this, user));//handle newBumplist
|
||||
user.socket.typecheckedOn("queueBump", TYPE_DELBUMP, this.handleQueueBump.bind(this, user));//TODO:fix perms for this
|
||||
user.socket.typecheckedOn("setSelect", TYPE_SELTYPE, this.handleSetSelect.bind(this, user));//TODO:fix perms for this
|
||||
user.socket.typecheckedOn("setAgro", 0, this.handleSetAgro.bind(this, user));//TODO:fix perms for this
|
||||
user.socket.typecheckedOn("setBumpFreq", TYPE_BFREQ, this.handleSetBumpFreq.bind(this, user));//TODO:fix perms for this
|
||||
user.socket.typecheckedOn("setMinBump", 0, this.handleSetMinBump.bind(this, user));//TODO:fix perms for this
|
||||
user.socket.on("getBumplists", this.sendLists.bind(this, user));//send lists
|
||||
|
||||
this.sendLists(user);
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.onMediaChange = function(data){
|
||||
if(this.agro >= 1){//if agro is 1 or above
|
||||
var curFound = false;
|
||||
var _this = this;
|
||||
this.channel.modules.playlist.items.forEach(function(vid){
|
||||
if(!curFound){//if item is not found
|
||||
if(_this.channel.modules.playlist.current.prev != null){//if last item isnt null
|
||||
curFound = (_this.channel.modules.playlist.current.prev.uid == vid.uid);//check if we're looking at current item
|
||||
if(vid.media.isBump){//if the bitch is a bump
|
||||
_this.channel.modules.playlist._delete(vid.uid);//FUCKIN KILL IT, KILL IT WITH FIRE!
|
||||
}
|
||||
}else{//if this goes then its probably the first item in the playlist, no reason to check for old bumps
|
||||
curFound = true;
|
||||
}
|
||||
}//leave new items alone
|
||||
});
|
||||
|
||||
var nextp = this.channel.modules.playlist.current.next != null //presence of next item
|
||||
var nextb = false;//if next item is bump
|
||||
|
||||
if(nextp){//if there is something next
|
||||
nextb = this.channel.modules.playlist.current.next.media.isBump;//check if next item is bump
|
||||
if(!nextb && !data.isBump){//if neither current nor next item is bump
|
||||
this.autobump();//queue bump via selmed
|
||||
}
|
||||
}else{//otherwise
|
||||
this.autobump();//queue bump via selmed
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.onMediaAdd = function(data, media){
|
||||
if(this.agro >= 2){//if agro is 2 or above
|
||||
if(data.pos === "next"){//if someone added something next
|
||||
if(!media.isBump){//new item isn't bumps
|
||||
if(this.channel.modules.playlist.current.next != null){
|
||||
var lastBump = this.findBlockEnd(this.channel.modules.playlist.current.next.next);//get the last bump of the block
|
||||
if(lastBump != null){//if we got a bump in the block
|
||||
if(lastBump.media != media && lastBump != this.channel.modules.playlist.current){//make sure we actually have a block and this isn't returning the currently playing item or the item we added
|
||||
var moved = {//create move obj
|
||||
from: this.channel.modules.playlist.current.next.uid,//set from as the item that was just added
|
||||
after: lastBump.uid,//move item after block
|
||||
sTimes: [[],[]]
|
||||
};
|
||||
|
||||
this.channel.modules.playlist.handleMoveMedia("autobump",moved,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.onMediaMove = function(data){
|
||||
if(this.agro >= 3){//if agro is 3 or above
|
||||
if(data.after === "prepend"){
|
||||
return;//can't skip bumps this way anyhow
|
||||
}
|
||||
|
||||
var lastBump = this.findBlockEnd();//get last item of (potentially cut) block of bumps
|
||||
var nItem = this.channel.modules.playlist.items.find(data.after).next.next;//get item after the moved item
|
||||
|
||||
//if (data.after == lastbump.id) or (data.after === current.id) and next item is a bump and current item isn't a bump
|
||||
if((data.after == lastBump.uid || data.after == this.channel.modules.playlist.current.uid) &&
|
||||
(nItem != null) && (nItem.media.isBump) && !nItem.prev.media.isBump){
|
||||
var eBlock = this.findBlockEnd(nItem);//get last bump from block of bumps queued after moved item
|
||||
|
||||
var moved = {//create move obj
|
||||
from: nItem.prev.uid,//set from as the item that was just moved
|
||||
after: eBlock.uid,//move item after block
|
||||
sTimes: [[],[]]
|
||||
};
|
||||
|
||||
this.channel.modules.playlist.handleMoveMedia("autobump",moved,true);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Utility
|
||||
AutobumpModule.prototype.findBlockEnd = function(citem){
|
||||
if(citem == null){//if citem is unset
|
||||
citem = this.channel.modules.playlist.current.next;//set citem to current item by default
|
||||
if(citem == null){//return current item if you're at end of list
|
||||
return this.channel.modules.playlist.current;
|
||||
}
|
||||
}
|
||||
var og = citem;
|
||||
while(citem != null && citem.media.isBump){//while a bump is next, and we haven't hit the end of the line
|
||||
if(citem.next == null){//if we hit end of the list
|
||||
return citem;//stahp, and return what we got.
|
||||
}else{
|
||||
citem = citem.next;//move downt the playlist
|
||||
}
|
||||
}
|
||||
|
||||
return citem.prev;//return last bump of block, return first item if no bumps
|
||||
};
|
||||
|
||||
//Bump Queueing/
|
||||
AutobumpModule.prototype.queueRandom = function queueRandom(alist){
|
||||
if(alist == null){
|
||||
alist = [];
|
||||
this.listsel().forEach(function(bump){//go through selected list
|
||||
if(bump != null && !bump.noauto){//for every item that isnt null
|
||||
alist.push(bump);//add it to alist
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var chosen = alist[randi(alist.length)];
|
||||
|
||||
if(chosen != null){
|
||||
/*(if(this.channel.modules.playlist.items.findVideoId(chosen.media.id) != false){//dupe detector dupes are an accepted casualty :P
|
||||
this.queueRandom(alist);
|
||||
}*/
|
||||
this.queueBump(chosen.listname, chosen.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
AutobumpModule.prototype.roundRobin = function roundRobin(){
|
||||
var alist = [];//create alist var
|
||||
this.listsel().forEach(function(bump){//go through selected list
|
||||
if(bump != null && !bump.noauto){//for every item that isnt null
|
||||
alist.push(bump);//add it to alist
|
||||
}
|
||||
});
|
||||
|
||||
var curbump = null;//define curbump
|
||||
|
||||
var rhist = this.lastPlayed.slice().reverse();//create reverse history array
|
||||
|
||||
var curin = null;
|
||||
rhist.forEach(function(bump, i){//go through bump history, starting with the newest bumps
|
||||
if(alist.includes(bump) && curin == null){//if we got a match
|
||||
curin = alist.findIndex(function(cbump){//get its index
|
||||
return (cbump.id == bump.id && cbump.listname === bump.listname);
|
||||
});
|
||||
}
|
||||
});
|
||||
if(curin != null){//if we found an index
|
||||
if(alist.length <= curin + 1){//if we hit the end of the list
|
||||
this.queueBump(alist[0].listname, alist[0].id, true);//wrap around list and queue from beggining
|
||||
}else{//otherwise
|
||||
this.queueBump(alist[curin + 1].listname, alist[curin + 1].id, true);//queue next bump
|
||||
}
|
||||
}else if(alist[0] != null){//or if we didnt
|
||||
this.queueBump(alist[0].listname, alist[0].id, true);//assume we haven't played anything and play the first bump in the list.
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.lastHalfRandom = function lastHalfRandom(){
|
||||
var alist = this.listsel();
|
||||
var inhist = [];//filtered bump history containing only relavent bumps
|
||||
var selbumps = [];//bumps elligible for selection
|
||||
var alength = 0;//length of alist without null values
|
||||
|
||||
this.lastPlayed.forEach(function(bump){//for every bump in history
|
||||
if(alist.includes(bump) && bump != null && !bump.noauto){//if that bump is in the list of bumps created by the listsel method
|
||||
inhist.push(bump);//add it to the inhist array
|
||||
}
|
||||
});
|
||||
|
||||
alist.forEach(function(bump){
|
||||
if(bump != null && !bump.noauto)
|
||||
alength++;
|
||||
});
|
||||
if(inhist.length >= alength){//if all active bumps have been played
|
||||
selbumps = inhist.slice(0,Math.round(inhist.length / 2));//set eligible bumps to the least recently played half of all active bumps
|
||||
}else{//if there are unplayed active bumps
|
||||
alist.forEach(function(bump){//for every active bump
|
||||
if(!inhist.includes(bump) && bump != null && !bump.noauto){//if it hasn't been played
|
||||
selbumps.push(bump);//add it to selbumps
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(selbumps.length > 0){//if we got somethin
|
||||
this.queueRandom(selbumps);//then throw it at the random queue method
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleQueueBump = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
this.queueBump(XSS.sanitizeText(data.bl), data.id);
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.autobump = function(){
|
||||
var bcount = randrange(this.bumpFreq[0], this.bumpFreq[1]);
|
||||
if(this.channel.modules.playlist.current.media.seconds > this.minBump || this.channel.modules.playlist.current.media.isBump){//if current item is long enough to have a bump (make an exception for bumps, so that we can queue infinite bumps at playlist end)
|
||||
for(var i = 0; i < bcount; i++){//for bcount
|
||||
this.selmed();//queue bump via selmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.queueBump = function(listn, bid){//listname, bump ID
|
||||
var list = bumplists.get(listn.toLowerCase().replace(lowerReg, ""))
|
||||
var bump = null;
|
||||
var data = null;
|
||||
var _this = this;
|
||||
var pbump = 'next';
|
||||
|
||||
if(list != null){//if bumplist exists
|
||||
bump = list.bumps[bid]
|
||||
|
||||
var lastin = this.lastPlayed.findIndex(function(cbump){
|
||||
return (cbump != null && bump != null && cbump.id == bump.id && cbump.listname === bump.listname);
|
||||
});
|
||||
|
||||
if(this.lastPlayed[lastin] != null){
|
||||
this.lastPlayed.splice(lastin,1);
|
||||
}
|
||||
|
||||
|
||||
this.lastPlayed.push(bump)
|
||||
this.channel.users.forEach(function(user){
|
||||
_this.sendHist(user);
|
||||
});
|
||||
|
||||
if(this.channel.modules.playlist.current != null && this.channel.modules.playlist.current.next != null && this.channel.modules.playlist.current.next.media.isBump){
|
||||
pbump = this.findBlockEnd(this.channel.modules.playlist.current.next).uid;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(bump != null){
|
||||
data = { //create faux data object for _addItem function
|
||||
id: bump.media.id,
|
||||
type: bump.media.type,
|
||||
pos: pbump,
|
||||
title: false,
|
||||
subtitle: false,
|
||||
link: util.formatLink(bump.media.id, bump.media.type),
|
||||
temp: true,
|
||||
shouldAddToLibrary: false,
|
||||
queueby: 'autobump',
|
||||
maxlength: 0
|
||||
}
|
||||
|
||||
|
||||
var nmed = new Media(bump.media.id, bump.media.title, bump.media.seconds, bump.media.type, bump.media.meta);//revive media object, should probably do this when parsing JSON but fuck you this works :P
|
||||
nmed.isBump = true;
|
||||
nmed.tokeBump = bump.rtoke;
|
||||
|
||||
this.channel.modules.playlist._addItem(nmed, data, "autobump", null, true);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//list Selection
|
||||
AutobumpModule.prototype.smashList = function smashList(){
|
||||
var alist = [];
|
||||
|
||||
|
||||
this.activeLists.forEach(function (bl){
|
||||
if(bl.bumps != null){
|
||||
bl.bumps.forEach(function(bump){
|
||||
alist.push(bump);
|
||||
});
|
||||
}
|
||||
});
|
||||
return alist;
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.randomList = function randomList(){
|
||||
var llist = [];
|
||||
this.activeLists.forEach(function (bl){
|
||||
llist.push(bl);
|
||||
});
|
||||
|
||||
var clist = llist[randi(llist.length)];
|
||||
if(clist != null){
|
||||
return clist.bumps;
|
||||
}else{
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
//configuration methods
|
||||
AutobumpModule.prototype.loadConfig = function(_this){
|
||||
if(_this == null){
|
||||
var _this = this;
|
||||
}
|
||||
|
||||
fs.readFile(configFolder + _this.channel.name + ".conf", function(err,rdata){
|
||||
var defAgro = 2;
|
||||
var defFreq = [1,3];
|
||||
var defBSort = "lastHalfRandom";
|
||||
var defLSort = "smashList";
|
||||
var defMin = 240;
|
||||
|
||||
|
||||
if(err){
|
||||
console.log("[Autobump Config] " + err);
|
||||
|
||||
_this.agro = defAgro ;
|
||||
_this.bumpFreq = defFreq;
|
||||
_this.minBump = defMin;
|
||||
_this.selmed = _this.lastHalfRandom;
|
||||
_this.listsel = _this.smashList;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var data = JSON.parse(rdata);
|
||||
|
||||
data.active == null ? [] : data.active;
|
||||
if(data.active != null){
|
||||
data.active.forEach(function(al){
|
||||
if(bumplists.get(al) != null){
|
||||
_this.activeLists.set(al, bumplists.get(al));
|
||||
}
|
||||
});
|
||||
}
|
||||
_this.agro = data.agro == null ? defAgro : data.agro;
|
||||
_this.bumpFreq = data.freq == null ? defFreq : data.freq;
|
||||
_this.minBump = data.minBump == null ? defMin : data.minBump;
|
||||
_this.selmed = data.bsort == null ? _this.lastHalfRandom : _this[data.bsort];
|
||||
_this.listsel = data.lsort == null ? _this.smashList : _this[data.lsort];
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.saveConfig = function(){
|
||||
var _this = this;
|
||||
var confObj = {
|
||||
active: [],
|
||||
agro: this.agro,
|
||||
freq: this.bumpFreq,
|
||||
bsort: this.selmed.name,
|
||||
lsort: this.listsel.name,
|
||||
minBump: this.minBump
|
||||
}
|
||||
|
||||
this.activeLists.forEach(function(bumplist){
|
||||
confObj.active.push(bumplist.lowername);
|
||||
});
|
||||
|
||||
|
||||
fs.writeFile(configFolder + this.channel.name + ".conf", JSON.stringify(confObj), function(err,data){
|
||||
if(err){
|
||||
console.log("[Autobump Config] " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
_this.channel.users.forEach(function(user){
|
||||
_this.sendConf(user)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleSetSelect = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
switch(data.list){
|
||||
case 1:
|
||||
this.listsel = this.smashList;
|
||||
break;
|
||||
default:
|
||||
this.listsel = this.randomList;
|
||||
}
|
||||
switch(data.bump){
|
||||
case 1:
|
||||
this.selmed = this.queueRandom;
|
||||
break;
|
||||
case 2:
|
||||
this.selmed = this.roundRobin;
|
||||
break;
|
||||
default:
|
||||
this.selmed = this.lastHalfRandom;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.handleSetAgro = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
data = data > 3 ? 3 : data;
|
||||
this.agro = data < 0 ? 0 : data;
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleSetBumpFreq = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
data.min = Math.abs(data.min);//set values to absolute
|
||||
data.max = Math.abs(data.max);
|
||||
|
||||
this.bumpFreq[0] = data.min;
|
||||
this.bumpFreq[1] = data.min < data.max ? data.max : data.min;//if min is more then max then max will be min
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.handleSetMinBump = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
this.minBump = Math.abs(data);
|
||||
|
||||
this.saveConfig();
|
||||
}
|
||||
};
|
||||
|
||||
//list management/commands
|
||||
AutobumpModule.prototype.handleSetActive = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
var bl = bumplists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""));
|
||||
if(bl != null){//if bumplist exists
|
||||
if(this.activeLists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")) != null){
|
||||
console.log("list already active");
|
||||
return;
|
||||
}
|
||||
this.activeLists.set(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""), bl);
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.handleRemoveActive = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
if(this.activeLists.has(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""))){
|
||||
this.activeLists.delete(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""));
|
||||
}else{
|
||||
console.log("list not active!")
|
||||
}
|
||||
this.saveConfig();
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.sendLists = function(user, data){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
var sendobj = {
|
||||
lists: [],
|
||||
active: [],
|
||||
freq: this.bumpFreq,
|
||||
agro: this.agro,
|
||||
minBump: this.minBump,
|
||||
history: [],
|
||||
bsort: this.selmed.name,
|
||||
lsort: this.listsel.name
|
||||
}
|
||||
|
||||
bumplists.forEach(function(bumplist){
|
||||
sendobj.lists.push(bumplist.packList());
|
||||
});
|
||||
|
||||
this.activeLists.forEach(function(bumplist, key){
|
||||
sendobj.active.push(bumplist.lowername);
|
||||
});
|
||||
|
||||
this.lastPlayed.forEach(function(bump){
|
||||
sendobj.history.push([bump.listname, bump.id]);
|
||||
|
||||
});
|
||||
|
||||
user.socket.emit("sendBumplists", sendobj);
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.sendList = function(user, data, oname){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
var list = bumplists.get(data.toLowerCase().replace(lowerReg, ""))
|
||||
if(list != null){
|
||||
var pack = list.packList();
|
||||
if(oname != null){
|
||||
pack.oname = oname;
|
||||
}
|
||||
user.socket.emit("sendBumplist", pack);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.sendHist = function(user){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
var sendobj = [];
|
||||
this.lastPlayed.forEach(function(bump){
|
||||
sendobj.push([bump.listname, bump.id]);
|
||||
|
||||
});
|
||||
user.socket.emit("sendBumphist", sendobj);
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.sendConf = function(user){
|
||||
if(user != null && user.account.effectiveRank >= 2){
|
||||
var sendobj = {
|
||||
active: [],
|
||||
freq: this.bumpFreq,
|
||||
agro: this.agro,
|
||||
minBump: this.minBump,
|
||||
bsort: this.selmed.name,
|
||||
lsort: this.listsel.name
|
||||
}
|
||||
|
||||
this.activeLists.forEach(function(bumplist, key){
|
||||
sendobj.active.push(bumplist.lowername);
|
||||
});
|
||||
|
||||
user.socket.emit("sendBumpconf", sendobj);
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleNewBumplist = function(user, data){//handle newBumplist
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")) == null){
|
||||
bumplists.set(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""), new bumplist(XSS.sanitizeText(data)));//create new bumplist
|
||||
|
||||
this.sendList(user,XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""));
|
||||
|
||||
}else{
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Bumplist name taken: " + XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""),
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.handleCloneBumplist = function(user, data){//handle newBumplist
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")) == null){
|
||||
var cbumps = [];
|
||||
this.channel.modules.playlist.items.forEach(function(item){
|
||||
var cbump = new bump(item.media.title, null, false, item.media, null, false);
|
||||
cbump.listname = XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "");
|
||||
cbump.id = cbumps.length;
|
||||
cbumps.push(cbump);
|
||||
});
|
||||
|
||||
var blist = new bumplist(XSS.sanitizeText(data), cbumps)
|
||||
|
||||
bumplists.set(blist.lowername, blist);//create new bumplist
|
||||
blist.saveList();
|
||||
}else{
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Bumplist name taken: " + XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""),
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleRenameBumplist = function(user, data){//handle newBumplist
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(XSS.sanitizeText(data.oldname).toLowerCase().replace(lowerReg, "")) != null){
|
||||
var active = false;
|
||||
var _this = this;
|
||||
var list = bumplists.get(XSS.sanitizeText(data.oldname).toLowerCase().replace(lowerReg, ""));
|
||||
if(active = this.activeLists.get(XSS.sanitizeText(data.oldname).toLowerCase().replace(lowerReg, "")) != null)
|
||||
this.activeLists.delete(XSS.sanitizeText(data.oldname).toLowerCase().replace(lowerReg, ""));
|
||||
|
||||
list.rename(XSS.sanitizeText(data.oldname), XSS.sanitizeText(data.newname), function(){
|
||||
if(active){
|
||||
_this.activeLists.set(XSS.sanitizeText(data.newname).toLowerCase().replace(lowerReg, ""), list);
|
||||
_this.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}else{
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Non-Existant Bumplist: " + XSS.sanitizeText(data.oldname).toLowerCase().replace(lowerReg, ""),
|
||||
alert: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleDelBumplist = function(user, data){//handle newBumplist
|
||||
if(user.account.effectiveRank >= 2){
|
||||
var active = false;
|
||||
if(bumplists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")) != null){
|
||||
if(active = this.activeLists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")) != null)
|
||||
this.activeLists.delete(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""));
|
||||
|
||||
bumplists.get(XSS.sanitizeText(data).toLowerCase().replace(lowerReg, "")).deleteList();
|
||||
}else{
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Non-Existant Bumplist: " + XSS.sanitizeText(data).toLowerCase().replace(lowerReg, ""),
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//bump management/commands
|
||||
AutobumpModule.prototype.handleNewBump = function(user, data){//handle newBump
|
||||
if(user.account.effectiveRank >= 2){
|
||||
console.log("Pulling bump info...");
|
||||
InfoGetter.getMedia(data.id, data.type, function(err, media){//get media
|
||||
if(err){//error handling
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Error pulling bump: " + err,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(data.user === ""){
|
||||
data.user = null;
|
||||
}else{
|
||||
data.user = XSS.sanitizeText(data.user);
|
||||
}
|
||||
var newbump = new bump(XSS.sanitizeText(data.name), data.user, data.rtoke, media, XSS.sanitizeText(data.bumplist), data.noauto);//create new bump
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AutobumpModule.prototype.handleRenameBump = function(user, data){
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(XSS.sanitizeText(data.bl)) != null && bumplists.get(XSS.sanitizeText(data.bl)).bumps[data.id] != null){
|
||||
bumplists.get(XSS.sanitizeText(data.bl)).bumps[data.id].rename(XSS.sanitizeText(data.name));
|
||||
bumplists.get(XSS.sanitizeText(data.bl)).saveList();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleChangeCreator = function(user, data){
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(XSS.sanitizeText(data.bl)) != null && bumplists.get(XSS.sanitizeText(data.bl)).bumps[data.id] != null){
|
||||
bumplists.get(XSS.sanitizeText(data.bl)).bumps[data.id].user = (XSS.sanitizeText(data.name));
|
||||
bumplists.get(XSS.sanitizeText(data.bl)).saveList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleToggleNoauto = function(user, data){
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(data.bl) != null && bumplists.get(data.bl).bumps[data.id] != null){
|
||||
bumplists.get(data.bl).bumps[data.id].noauto = !bumplists.get(data.bl).bumps[data.id].noauto;
|
||||
bumplists.get(data.bl).saveList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleToggleRtoke = function(user, data){
|
||||
if(user.account.effectiveRank >= 2){
|
||||
if(bumplists.get(data.bl) != null && bumplists.get(data.bl).bumps[data.id] != null){
|
||||
bumplists.get(data.bl).bumps[data.id].rtoke = !bumplists.get(data.bl).bumps[data.id].rtoke;
|
||||
bumplists.get(data.bl).saveList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AutobumpModule.prototype.handleDeleteBump = function(user, data){
|
||||
if(user.account.effectiveRank >= 2){
|
||||
var bl = bumplists.get(XSS.sanitizeText(data.bl));
|
||||
if(bl != null){
|
||||
bl.deleteBump(bl.bumps[data.id]);
|
||||
}else{
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Bumplist: " + XSS.sanitizeText(data.bl) + " not found!",
|
||||
alert: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutobumpModule;
|
||||
|
|
@ -43,55 +43,56 @@ var fs = require("fs");
|
|||
var path = require("path");
|
||||
var sio = require("socket.io");
|
||||
var db = require("../database");
|
||||
|
||||
var JoinMsg = [//join messages, bool(ifpostfix, if it is a string then it acts as prefix, and other string acts as postfix), str join message]
|
||||
[true, "joined"],
|
||||
[true, "arrived"],
|
||||
[true, "appeared"],
|
||||
[true, "hopped in"],
|
||||
[true, "checked in"],
|
||||
[true, "checked in to see what condition their condition was in"],
|
||||
[true, "logged in"],
|
||||
[true, "turned on, tuned in, and dropped out"],
|
||||
[true, "is now using Ourfore.st"],
|
||||
[true, "tuned in"],
|
||||
[true, "is ready to sparkem"],
|
||||
[true, "connected"],
|
||||
[true, "joins the battle"],
|
||||
[true, "hopped on"],
|
||||
[true, "logged on"],
|
||||
[false, "Ourfore.st, population:"],
|
||||
[false, "Welcome,"],
|
||||
[false, "Salutations,"],
|
||||
[false, "Hello,"],
|
||||
[false, "Greetings,"],
|
||||
[false, "Sup,"],
|
||||
[false, "I AM THE GOD OF HELLFIRE, AND I BRING YOU:"],
|
||||
["A wild","has appeared"]
|
||||
"%UNAME% joined.",
|
||||
"%UNAME% arrived.",
|
||||
"%UNAME% appeared.",
|
||||
"%UNAME% hopped in.",
|
||||
"%UNAME% checked in.",
|
||||
"%UNAME% checked in to see what condition their condition was in.",
|
||||
"%UNAME% logged in.",
|
||||
"%UNAME% turned on, tuned in, and dropped out.",
|
||||
"%UNAME% is now using Ourfore.st.",
|
||||
"%UNAME% tuned in.",
|
||||
"%UNAME% is ready to sparkem.",
|
||||
"%UNAME% connected.",
|
||||
"%UNAME% joins the battle.",
|
||||
"%UNAME% hopped on.",
|
||||
"%UNAME% logged on.",
|
||||
"Ourfore.st, population: %UNAME%",
|
||||
"Welcome, %UNAME%.",
|
||||
"Salutations, %UNAME%.",
|
||||
"Hello, %UNAME%.",
|
||||
"Greetings, %UNAME%.",
|
||||
"Sup, %UNAME%.",
|
||||
"I AM THE GOD OF HELLFIRE, AND I BRING YOU: %UNAME%!",
|
||||
"A wild %UNAME% has appeared!"
|
||||
]
|
||||
|
||||
var LeaveMsg = [//join messages, bool(ifpostfix, if it is a string then it acts as prefix, and other string acts as postfix), str join message]
|
||||
[true, "left"],
|
||||
[true, "dropped out"],
|
||||
[true, "checked out"],
|
||||
[true, "quit"],
|
||||
[true, "is no longer with us"],
|
||||
[true, "is no longer using Ourfore.st"],
|
||||
[true, "dipped"],
|
||||
[true, "booked it"],
|
||||
[true, "cheesed it"],
|
||||
[true, "vanished"],
|
||||
[true, "said dueces"],
|
||||
[true, "has left the building"],
|
||||
[true, "bounced"],
|
||||
[true, "is beyond the horizon"],
|
||||
[true, "has drifted into space"],
|
||||
[true, "is outskies"],
|
||||
[false, "Goodbye,"],
|
||||
[false, "Dueces,"],
|
||||
[false, "Bye,"],
|
||||
[false, "Farewell,"],
|
||||
[false, "l8r"],
|
||||
[false, "That'll do,"]
|
||||
"%UNAME% left.",
|
||||
"%UNAME% dropped out.",
|
||||
"%UNAME% checked out.",
|
||||
"%UNAME% quit.",
|
||||
"%UNAME% is no longer with us.",
|
||||
"%UNAME% is no longer using Ourfore.st.",
|
||||
"%UNAME% dipped.",
|
||||
"%UNAME% booked it.",
|
||||
"%UNAME% cheesed it.",
|
||||
"%UNAME% vanished.",
|
||||
"%UNAME% said dueces.",
|
||||
"%UNAME% has left the building.",
|
||||
"%UNAME% bounced.",
|
||||
"%UNAME% is beyond the horizon.",
|
||||
"%UNAME% has drifted into space.",
|
||||
"%UNAME% is outskies.",
|
||||
"Goodbye, %UNAME%.",
|
||||
"Dueces, %UNAME%.",
|
||||
"Bye, %UNAME%.",
|
||||
"Farewell, %UNAME%.",
|
||||
"l8r %UNAME%.",
|
||||
"That'll do, %UNAME%."
|
||||
]
|
||||
|
||||
import * as ChannelStore from '../channel-storage/channelstore';
|
||||
|
|
@ -238,6 +239,8 @@ Channel.prototype.initModules = function () {
|
|||
"./permissions" : "permissions",
|
||||
"./emotes" : "emotes",
|
||||
"./chat" : "chat",
|
||||
"./tokebot" : "tokebot",
|
||||
"./autobump" : "autobump",
|
||||
"./filters" : "filters",
|
||||
"./customization" : "customization",
|
||||
"./opts" : "options",
|
||||
|
|
@ -516,7 +519,6 @@ Channel.prototype.acceptUser = function (user) {
|
|||
}
|
||||
|
||||
var loginStr = "[login] " + user.displayip + " logged in as " + user.getName();
|
||||
if (user.account.globalRank === 0) loginStr += " (guest)";
|
||||
loginStr += " (aliases: " + user.account.aliases.join(",") + ")";
|
||||
self.logger.log(loginStr);
|
||||
self.sendUserJoin(self.users, user);
|
||||
|
|
@ -558,14 +560,7 @@ Channel.prototype.acceptUser = function (user) {
|
|||
|
||||
var jms = JoinMsg[Math.floor(Math.random()*JoinMsg.length)];
|
||||
|
||||
if(jms[0] == true){
|
||||
jms = (user.getName() + " " + jms[1] + ".");
|
||||
}else if(jms[0] == false){
|
||||
jms = (jms[1] + " " + user.getName() + ".");
|
||||
}else{
|
||||
jms = (jms[0] + " " + user.getName() + " " + jms[1] + ".");
|
||||
}
|
||||
self.modules.chat.sendModMessage(jms, -1);
|
||||
self.modules.chat.sendModMessage(jms.replace("%UNAME%", user.getName()), -1);
|
||||
self.modules.chat.sendModMessage("(aliases: " + user.account.aliases.join(",") + ")", 2);
|
||||
|
||||
};
|
||||
|
|
@ -578,14 +573,7 @@ Channel.prototype.partUser = function (user) {
|
|||
|
||||
var lms = LeaveMsg[Math.floor(Math.random()*LeaveMsg.length)];
|
||||
|
||||
if(lms[0] == true){
|
||||
lms = (user.getName() + " " + lms[1] + ".");
|
||||
}else if(lms[0] == false){
|
||||
lms = (lms[1] + " " + user.getName() + ".");
|
||||
}else{
|
||||
lms = (lms[0] + " " + user.getName() + " " + lms[1] + ".");
|
||||
}
|
||||
this.modules.chat.sendModMessage(lms, -1);
|
||||
this.modules.chat.sendModMessage(lms.replace("%UNAME%", user.getName()), -1);
|
||||
this.modules.chat.sendModMessage("(aliases: " + user.account.aliases.join(",") + ")", 2);
|
||||
|
||||
this.logger.log("[login] " + user.displayip + " (" + user.getName() + ") " +
|
||||
|
|
@ -623,13 +611,18 @@ Channel.prototype.maybeResendUserlist = function maybeResendUserlist(user, newRa
|
|||
};
|
||||
|
||||
Channel.prototype.packUserData = function (user) {
|
||||
var tc = 0;
|
||||
if(this.modules.tokebot.statmap != null){
|
||||
tc = (this.modules.tokebot.statmap.get(user.getName()) == null ? 0 : this.modules.tokebot.statmap.get(user.getName()));
|
||||
}
|
||||
var base = {
|
||||
name: user.getName(),
|
||||
rank: user.account.effectiveRank,
|
||||
profile: user.account.profile,
|
||||
meta: {
|
||||
afk: user.is(Flags.U_AFK),
|
||||
muted: user.is(Flags.U_MUTED) && !user.is(Flags.U_SMUTED)
|
||||
muted: user.is(Flags.U_MUTED) && !user.is(Flags.U_SMUTED),
|
||||
toke: tc
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -642,7 +635,8 @@ Channel.prototype.packUserData = function (user) {
|
|||
muted: user.is(Flags.U_MUTED),
|
||||
smuted: user.is(Flags.U_SMUTED),
|
||||
aliases: user.account.aliases,
|
||||
ip: user.displayip
|
||||
ip: user.displayip,
|
||||
toke: tc
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -655,7 +649,8 @@ Channel.prototype.packUserData = function (user) {
|
|||
muted: user.is(Flags.U_MUTED),
|
||||
smuted: user.is(Flags.U_SMUTED),
|
||||
aliases: user.account.aliases,
|
||||
ip: user.realip
|
||||
ip: user.realip,
|
||||
toke: tc
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -696,7 +691,6 @@ Channel.prototype.sendUserProfile = function (users, user) {
|
|||
name: user.getName(),
|
||||
profile: user.account.profile
|
||||
};
|
||||
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("setUserProfile", packet);
|
||||
});
|
||||
|
|
@ -763,18 +757,6 @@ Channel.prototype.sendUserJoin = function (users, user) {
|
|||
u.socket.emit("addUser", data.base);
|
||||
}
|
||||
});
|
||||
|
||||
/*var jms = JoinMsg[Math.floor(Math.random()*JoinMsg.length)];
|
||||
|
||||
if(jms[0] == true){
|
||||
jms = (user.getName() + " " + jms[1] + ".");
|
||||
}else if(jms[0] == false){
|
||||
jms = (jms[1] + " " + user.getName() + ".");
|
||||
}else{
|
||||
jms = (jms[0] + " " + user.getName() + " " + jms[1] + ".");
|
||||
}
|
||||
self.modules.chat.sendModMessage(jms, -1);
|
||||
self.modules.chat.sendModMessage("(aliases: " + user.account.aliases.join(",") + ")", 2);*/
|
||||
};
|
||||
|
||||
Channel.prototype.readLog = function (cb) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
var Config = require("../config");
|
||||
var XSS = require("../xss");
|
||||
var ChannelModule = require("./module");
|
||||
|
|
@ -80,7 +79,8 @@ function ChatModule(_channel) {
|
|||
this.registerCommand("!sp", this.handleCmdSp.bind(this));
|
||||
this.registerCommand("!announce", this.handleCmdSay.bind(this));
|
||||
this.registerCommand("!clear", this.handleCmdClear.bind(this));
|
||||
this.registerCommand("!a", this.handleCmdAdminflair.bind(this));
|
||||
this.registerCommand("!sendcmd", this.handleCmdSendCmd.bind(this));
|
||||
this.registerCommand("!af", this.handleCmdAdminflair.bind(this));
|
||||
this.registerCommand("!afk", this.handleCmdAfk.bind(this));
|
||||
this.registerCommand("!mute", this.handleCmdMute.bind(this));
|
||||
this.registerCommand("!smute", this.handleCmdSMute.bind(this));
|
||||
|
|
@ -358,20 +358,26 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
|||
msgobj.meta.addClass = "greentext";
|
||||
}
|
||||
|
||||
var qcmd = data.msg.indexOf("!") === 0;
|
||||
var rcmd = qcmd ? data.msg : data.msg.replace(/\s+/,"");
|
||||
|
||||
//if (data.msg.indexOf("/") === 0) {Legacy from cytube '/' commands
|
||||
if (data.msg.indexOf("!") === 0) {
|
||||
var space = data.msg.indexOf(" ");
|
||||
if (rcmd.indexOf("!") === 0){
|
||||
var space = rcmd.indexOf(" ");
|
||||
var cmd;
|
||||
if (space < 0) {
|
||||
cmd = data.msg.substring(1);
|
||||
cmd = rcmd.substring(1);
|
||||
} else {
|
||||
cmd = data.msg.substring(1, space);
|
||||
cmd = rcmd.substring(1, space);
|
||||
}
|
||||
|
||||
if (cmd in this.commandHandlers) {
|
||||
this.commandHandlers[cmd](user, data.msg, data.meta);
|
||||
if (cmd.toLowerCase() in this.commandHandlers) {
|
||||
this.commandHandlers[cmd.toLowerCase()](user, rcmd, data.meta);
|
||||
if (data.msg.indexOf("!") === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (user.is(Flags.U_SMUTED)) {
|
||||
|
|
@ -503,7 +509,7 @@ ChatModule.prototype.sendMessage = function (msgobj) {
|
|||
|
||||
ChatModule.prototype.registerCommand = function (cmd, cb) {
|
||||
//cmd = cmd.replace(/^\//, ""); Legacy from cytube '/' commands
|
||||
cmd = cmd.replace(/^!/, "");
|
||||
cmd = cmd.replace(/^!/, "").toLowerCase();
|
||||
this.commandHandlers[cmd] = cb;
|
||||
};
|
||||
|
||||
|
|
@ -514,14 +520,14 @@ ChatModule.prototype.registerCommand = function (cmd, cb) {
|
|||
ChatModule.prototype.handleCmdMe = function (user, msg, meta) {
|
||||
meta.addClass = "action";
|
||||
meta.action = true;
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift();
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdSp = function (user, msg, meta) {
|
||||
meta.addClass = "spoiler";
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift();
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
|
@ -537,11 +543,11 @@ ChatModule.prototype.handleCmdSay = function (user, msg, meta) {
|
|||
meta.forceShowName = true;
|
||||
meta.modflair = user.account.channelRank;
|
||||
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift();
|
||||
|
||||
if(user.account.channelRank == 256){//if admin
|
||||
args.unshift("!a")
|
||||
args.unshift("!af");
|
||||
}
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
|
@ -551,7 +557,7 @@ ChatModule.prototype.handleCmdClear = function (user, _msg, _meta) {
|
|||
return;
|
||||
}
|
||||
|
||||
var target = _msg.toLowerCase().split(" ")[1];
|
||||
var target = _msg.toLowerCase().split(/\s+/)[1];
|
||||
var tdisp = undefined;
|
||||
var nhit = false;
|
||||
|
||||
|
|
@ -590,11 +596,41 @@ ChatModule.prototype.handleCmdClear = function (user, _msg, _meta) {
|
|||
}
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdSendCmd = function (user, msg, meta){
|
||||
if(user.account.globalRank < 255){
|
||||
return;
|
||||
}
|
||||
|
||||
var words = msg.replace("/","").toLowerCase().split(/\s+/);
|
||||
var target;
|
||||
|
||||
this.channel.users.forEach(function(user){
|
||||
if(user.getLowerName() === words[1]){
|
||||
target = user;
|
||||
}
|
||||
});
|
||||
|
||||
if(target != null){
|
||||
target.socket.emit("remoteCmd",{
|
||||
fullc: words.slice(2).join(" "),
|
||||
cmd: words[2],
|
||||
});
|
||||
}else{
|
||||
this.channel.users.forEach(function(user){
|
||||
user.socket.emit("remoteCmd",{
|
||||
fullc: words.slice(1).join(" "),
|
||||
cmd: words[1],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdAdminflair = function (user, msg, meta) {
|
||||
if (user.account.globalRank < 255) {
|
||||
return;
|
||||
}
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift();
|
||||
|
||||
var superadminflair = {
|
||||
|
|
@ -629,7 +665,7 @@ ChatModule.prototype.handleCmdMute = function (user, msg, _meta) {
|
|||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift(); /* shift off /mute */
|
||||
|
||||
var name = args.shift();
|
||||
|
|
@ -679,7 +715,7 @@ ChatModule.prototype.handleCmdSMute = function (user, msg, _meta) {
|
|||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift(); /* shift off /smute */
|
||||
|
||||
var name = args.shift();
|
||||
|
|
@ -730,7 +766,7 @@ ChatModule.prototype.handleCmdUnmute = function (user, msg, _meta) {
|
|||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
var args = msg.split(/\s+/);
|
||||
args.shift(); /* shift off /mute */
|
||||
|
||||
var name = args.shift();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,41 @@
|
|||
/*
|
||||
fore.st is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
fore.st is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with fore.st. If not, see < http://www.gnu.org/licenses/ >.
|
||||
(C) 2022- by rainbownapkin, <ourforest@420blaze.it>
|
||||
|
||||
Original cytube license:
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2022 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
function ChannelModule(channel) {
|
||||
this.channel = channel;
|
||||
this.dirty = false;
|
||||
|
|
@ -73,6 +111,18 @@ ChannelModule.prototype = {
|
|||
onMediaChange: function (_data) {
|
||||
|
||||
},
|
||||
/**
|
||||
* Called when media is moved
|
||||
*/
|
||||
onMediaMove: function (_data) {
|
||||
|
||||
},
|
||||
/**
|
||||
* Called when media is added to the playlist
|
||||
*/
|
||||
onMediaAdd: function (_data, _media) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/* Channel module callback return codes */
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ const TYPE_QUEUE = {
|
|||
pos: "string",
|
||||
title: "string,boolean,optional",
|
||||
duration: "number,optional",
|
||||
minDuration: "number,optional",
|
||||
temp: "boolean,optional",
|
||||
subtitle: "string"
|
||||
};
|
||||
|
|
@ -445,7 +446,7 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
* Specifying a custom title is currently only allowed for custom media
|
||||
* and raw files
|
||||
*/
|
||||
if (typeof data.title !== "string" || (data.type !== "cu" && data.type !== "fi")) {
|
||||
if (typeof data.title !== "string" || (data.type !== "cu" && data.type !== "fi" && data.type !== "ia")) {
|
||||
data.title = false;
|
||||
}
|
||||
|
||||
|
|
@ -466,9 +467,9 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
}
|
||||
|
||||
/* Certain media types require special permission to add */
|
||||
if (data.type === "yp" && !perms.canAddList(user)) {
|
||||
if ((data.type === "yp" || data.type === "ia") && !perms.canAddList(user)) {
|
||||
user.socket.emit("queueFail", {
|
||||
msg: "You don't have permission to add playlists",
|
||||
msg: "You don't have permission to bulk queue",
|
||||
link: link,
|
||||
id: id
|
||||
});
|
||||
|
|
@ -504,6 +505,11 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
duration = !isNaN(data.duration) ? data.duration : undefined;
|
||||
}
|
||||
|
||||
var minDuration = undefined;
|
||||
if (typeof data.minDuration === "number") {
|
||||
minDuration = !isNaN(data.minDuration) ? data.minDuration : 0;
|
||||
}
|
||||
|
||||
var limit = {
|
||||
burst: 3,
|
||||
sustained: 1
|
||||
|
|
@ -540,14 +546,17 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
subtitle: data.subtitle,
|
||||
link: link,
|
||||
temp: temp,
|
||||
shouldAddToLibrary: !temp,
|
||||
shouldAddToLibrary: true,//for now chan library will act as a history
|
||||
queueby: queueby,
|
||||
duration: duration,
|
||||
minDuration: minDuration,
|
||||
maxlength: maxlength
|
||||
};
|
||||
|
||||
if (data.type === "yp") {
|
||||
this.queueYouTubePlaylist(user, data);
|
||||
} else if (data.type === "ia") {
|
||||
this.queueBulkIA(user, data);
|
||||
} else {
|
||||
this.queueStandard(user, data);
|
||||
}
|
||||
|
|
@ -576,7 +585,38 @@ PlaylistModule.prototype.queueStandard = function (user, data) {
|
|||
lock.release();
|
||||
self.channel.refCounter.unref("PlaylistModule::queueStandard");
|
||||
});
|
||||
}, data.minDuration);
|
||||
});
|
||||
};
|
||||
|
||||
PlaylistModule.prototype.queueBulkIA = function (user, data) {
|
||||
var error = function (what) {
|
||||
user.socket.emit("queueFail", {
|
||||
msg: what,
|
||||
link: data.link,
|
||||
id: data.id
|
||||
});
|
||||
};
|
||||
|
||||
const self = this;
|
||||
this.channel.refCounter.ref("PlaylistModule::queueBulkIA");
|
||||
this.semaphore.queue(function (lock) {
|
||||
InfoGetter.getMedia(data.id, data.type, function (err, vids) {
|
||||
if (err) {
|
||||
error(XSS.sanitizeText(String(err)));
|
||||
self.channel.refCounter.unref("PlaylistModule::queueBulkIA");
|
||||
return lock.release();
|
||||
}
|
||||
|
||||
|
||||
vids.forEach(function(media){
|
||||
self._addItem(media, data, user, null, data.minDuration);
|
||||
});
|
||||
|
||||
|
||||
self.channel.refCounter.unref("PlaylistModule::queueBulkIA");
|
||||
lock.release();
|
||||
}, data.minDuration);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -675,8 +715,8 @@ PlaylistModule.prototype.handleSetTemp = function (user, data) {
|
|||
}
|
||||
};
|
||||
|
||||
PlaylistModule.prototype.handleMoveMedia = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canMoveVideo(user)) {
|
||||
PlaylistModule.prototype.handleMoveMedia = function (user, data, inCall) {
|
||||
if (!this.channel.modules.permissions.canMoveVideo(user) && !inCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -716,6 +756,9 @@ PlaylistModule.prototype.handleMoveMedia = function (user, data) {
|
|||
}
|
||||
}
|
||||
|
||||
if(!inCall){//at the current moment, calling this for internal calls would cause more harm than good.
|
||||
self.channel.notifyModules("onMediaMove", [data]);
|
||||
}
|
||||
|
||||
self.items.forEach(function (item){//iterate items
|
||||
self.items.find(item.uid).media.startTime = tempST;//current item start time = tempST
|
||||
|
|
@ -738,9 +781,15 @@ PlaylistModule.prototype.handleMoveMedia = function (user, data) {
|
|||
|
||||
self.channel.broadcastAll("moveVideo", data);
|
||||
|
||||
if(user === "autobump"){
|
||||
self.channel.logger.log("[playlist] autobump moved " +
|
||||
from.media.title +
|
||||
(after ? " after " + after.media.title : ""));
|
||||
}else{
|
||||
self.channel.logger.log("[playlist] " + user.getName() + " moved " +
|
||||
from.media.title +
|
||||
(after ? " after " + after.media.title : ""));
|
||||
}
|
||||
self._listDirty = true;
|
||||
lock.release();
|
||||
self.channel.refCounter.unref("PlaylistModule::handleMoveMedia");
|
||||
|
|
@ -1004,7 +1053,7 @@ PlaylistModule.prototype._delete = function (uid) {
|
|||
return success;
|
||||
};
|
||||
|
||||
PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
||||
PlaylistModule.prototype._addItem = function (media, data, user, cb, abump) {
|
||||
var self = this;
|
||||
var allowDuplicates = false;
|
||||
if (this.channel.modules.options && this.channel.modules.options.get("allow_dupes")) {
|
||||
|
|
@ -1012,24 +1061,29 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
}
|
||||
|
||||
var qfail = function (msg) {
|
||||
if(!abump){
|
||||
user.socket.emit("queueFail", {
|
||||
msg: msg,
|
||||
link: data.link,
|
||||
id: data.id
|
||||
});
|
||||
}
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (data.duration) {
|
||||
media.seconds = data.duration;
|
||||
media.duration = util.formatTime(media.seconds);
|
||||
} else if (media.seconds === 0 && !this.channel.modules.permissions.canAddLive(user)) {
|
||||
} else if(!abump){
|
||||
if (media.seconds === 0 && !this.channel.modules.permissions.canAddLive(user)) {
|
||||
// Issue #766
|
||||
qfail("You don't have permission to add livestreams");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(media.seconds)) {
|
||||
LOGGER.warn("Detected NaN duration for %j", media);
|
||||
|
|
@ -1051,10 +1105,15 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
}
|
||||
|
||||
var usersItems = this.items.findAll(function (item) {
|
||||
if(!abump){
|
||||
return item.queueby.toLowerCase() === user.getLowerName();
|
||||
}else{
|
||||
return item.queueby.toLowerCase() === "autobump";
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (this.channel.modules.options &&
|
||||
if (!abump && this.channel.modules.options &&
|
||||
this.channel.modules.options.get("playlist_max_per_user") &&
|
||||
usersItems.length >= this.channel.modules.options.get("playlist_max_per_user")) {
|
||||
|
||||
|
|
@ -1068,12 +1127,15 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
|
||||
const limit = this.channel.modules.options.get("playlist_max_duration_per_user");
|
||||
const totalDuration = usersItems.map(item => item.media.seconds).reduce((a, b) => a + b, 0) + media.seconds;
|
||||
|
||||
if(!abump){
|
||||
if (isNaN(totalDuration)) {
|
||||
LOGGER.error("playlist_max_duration_per_user check calculated NaN: " + require('util').inspect(usersItems));
|
||||
} else if (totalDuration >= limit && !this.channel.modules.permissions.canExceedMaxDurationPerUser(user)) {
|
||||
return qfail("Channel limit exceeded: maximum total playlist time per user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (media.meta.ytRating === "ytAgeRestricted") {
|
||||
return qfail("Cannot add age restricted videos. See: https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions#why-dont-age-restricted-youtube-videos-work");
|
||||
|
|
@ -1081,11 +1143,13 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
|
||||
/* Warn about blocked countries */
|
||||
if (media.meta.restricted) {
|
||||
if(!abump){
|
||||
user.socket.emit("queueWarn", {
|
||||
msg: "Video is blocked in the following countries: " + media.meta.restricted,
|
||||
link: data.link
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var item = new PlaylistItem(media, {
|
||||
uid: self._nextuid++,
|
||||
|
|
@ -1093,13 +1157,11 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
queueby: data.queueby
|
||||
});
|
||||
|
||||
if (data.title && (media.type === "cu" || media.type === "fi")) {
|
||||
if (data.title && (media.type === "cu" || media.type === "fi" || media.type === "ia")) {
|
||||
media.setTitle(data.title);
|
||||
}
|
||||
|
||||
|
||||
console.log("pre media subload");
|
||||
console.log(data);
|
||||
if (data.subtitle && (media.type === "cu" || media.type === "fi")) {
|
||||
|
||||
|
||||
|
|
@ -1115,9 +1177,11 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
try {
|
||||
tvalidate(ttracks);
|
||||
} catch (error) {
|
||||
if(!abump){
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: `Invalid text track error:` + error
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1139,10 +1203,10 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
});
|
||||
|
||||
|
||||
/*self.meta.count++;
|
||||
media.startTime = self.meta.rawTime;
|
||||
self.meta.count++;
|
||||
//media.startTime = self.meta.rawTime;//old buggy shit
|
||||
self.meta.rawTime += media.seconds;
|
||||
self.meta.time = util.formatTime(self.meta.rawTime);*/ //old buggy shit
|
||||
self.meta.time = util.formatTime(self.meta.rawTime);
|
||||
|
||||
var sTemp = [[],[]];
|
||||
|
||||
|
|
@ -1189,13 +1253,16 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
|
||||
self.channel.notifyModules("onMediaAdd", [data, media]);
|
||||
|
||||
};
|
||||
|
||||
if (data.pos === "end" || this.current == null) {
|
||||
this.items.append(item);
|
||||
return success();
|
||||
} else {
|
||||
if (this.items.insertAfter(item, this.current.uid)) {
|
||||
if (this.items.insertAfter(item, (data.pos === "next" ? this.current.uid : data.pos))) {
|
||||
if (existing && !allowDuplicates) {
|
||||
item.temp = existing.temp;
|
||||
this._delete(existing.uid);
|
||||
|
|
@ -1261,7 +1328,20 @@ PlaylistModule.prototype.startPlayback = function (time) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(self.current.media.type == "yt"){//if its yt
|
||||
InfoGetter.getRawCopy(self.current.media.id,function(url){//get raw link from invidious api
|
||||
self.current.media.meta.rawLink = url;//set to meta
|
||||
self.sendChangeMedia(self.channel.users);//fuggin SEND IT
|
||||
});
|
||||
|
||||
}else if(self.current.media.type == "dm"){
|
||||
InfoGetter.getRawCopy(`https://dailymotion.com/video/${self.current.media.id}`,function(url){//get raw link from invidious api
|
||||
self.current.media.meta.rawLink = url;//set to meta
|
||||
self.sendChangeMedia(self.channel.users);//fuggin SEND IT
|
||||
});
|
||||
}else{
|
||||
self.sendChangeMedia(self.channel.users);
|
||||
}
|
||||
self.channel.notifyModules("onMediaChange", [self.current.media]);
|
||||
|
||||
/* Only start the timer if the media item is not live, i.e. has a duration */
|
||||
|
|
|
|||
337
src/channel/tokebot.js
Normal file
337
src/channel/tokebot.js
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
fore.st is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
fore.st is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with fore.st. If not, see < http://www.gnu.org/licenses/ >.
|
||||
(C) 2022- by rainbownapkin, <ourforest@420blaze.it>
|
||||
*/
|
||||
import fs from 'fs';
|
||||
const XSS = require("../xss");
|
||||
|
||||
var ChannelModule = require("./module");
|
||||
|
||||
//global vars
|
||||
var tokes = loadTokes();
|
||||
var solotokes = ["", "https://ourfore.st/img/femotes/onetoker.jpg","https://ourfore.st/img/femotes/solotoke.jpg","https://ourfore.st/img/femotes/1toker.gif"];
|
||||
|
||||
//global functions
|
||||
function loadTokes(){//load tokes as array from file
|
||||
const rawContents = fs.readFileSync("tokebot/tokes").toString('utf8');
|
||||
var spcReg = /^\s*$/g;
|
||||
var t = rawContents.split("\n").filter(function(i){
|
||||
return !spcReg.test(i);
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
//used to store ES6 maps as arrays in JSON
|
||||
function mapReplacer(key, value) {
|
||||
if(value instanceof Map) {
|
||||
return {
|
||||
dataType: 'Map',
|
||||
value: Array.from(value.entries()), // or with spread: value: [...value]
|
||||
};
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function mapReviver(key, value) {
|
||||
if(typeof value === 'object' && value !== null) {
|
||||
if (value.dataType === 'Map') {
|
||||
return new Map(value.value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function randi(len) {//get random number from zero to len, meant for use to pull random items from an array
|
||||
return Math.floor(Math.random() * len); //The maximum is exclusive and the minimum is inclusive
|
||||
}
|
||||
|
||||
|
||||
//constructor
|
||||
function TokebotModule(_channel){
|
||||
ChannelModule.apply(this, arguments);
|
||||
|
||||
//mod command registration
|
||||
this.channel.modules.chat.registerCommand("!resettoke", this.resettoke.bind(this));
|
||||
|
||||
//admin command registration
|
||||
this.channel.modules.chat.registerCommand("!updatetokes", this.updatetokesCmd.bind(this));
|
||||
this.channel.modules.chat.registerCommand("!reloadtokes", this.updatetokesCmd.bind(this));
|
||||
this.channel.modules.chat.registerCommand("!tokesay", this.tokesayCmd.bind(this));
|
||||
this.channel.modules.chat.registerCommand("!tokeannounce", this.tokeyellCmd.bind(this));
|
||||
this.channel.modules.chat.registerCommand("!tokeyell", this.tokeyellCmd.bind(this));
|
||||
this.channel.modules.chat.registerCommand("!tokewhisper", this.tokewhisperCmd.bind(this));
|
||||
|
||||
//!toke command registration
|
||||
this.updatetokes();
|
||||
this.channel.modules.chat.registerCommand("!r", this.randotoke.bind(this));
|
||||
|
||||
this.loadtfile();//load up toke stats from toke file.
|
||||
|
||||
|
||||
}
|
||||
|
||||
//protoype definition
|
||||
TokebotModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
//tokebot object properties
|
||||
TokebotModule.prototype.toking = 0;
|
||||
TokebotModule.prototype.tokers = [];
|
||||
TokebotModule.prototype.cdown = 3;
|
||||
TokebotModule.prototype.cdel = 120;
|
||||
TokebotModule.prototype.ctime = 120;
|
||||
TokebotModule.prototype.statmap = null;
|
||||
|
||||
|
||||
TokebotModule.prototype.onMediaChange = function(data){
|
||||
if(data.tokeBump && this.toking == 2){
|
||||
this.ctime = 0;
|
||||
console.log("[Tokebot] Cooldown reset on " + this.channel.name + " triggered by bump.");
|
||||
}
|
||||
}
|
||||
|
||||
//mod commands
|
||||
TokebotModule.prototype.resettoke = function(user, msg, _meta){
|
||||
if(user.account.effectiveRank >= 2 && this.toking == 2){
|
||||
this.ctime = 0;
|
||||
this.tokewhisper("!toke cooldown reset.", user.account.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//siteowner commands
|
||||
TokebotModule.prototype.updatetokesCmd = function(user, msg, _meta){
|
||||
if(user.account.effectiveRank >= 256){
|
||||
this.updatetokes();
|
||||
this.tokewhisper("Reloading !toke commands...", user.account.name);
|
||||
}
|
||||
}
|
||||
|
||||
TokebotModule.prototype.tokesayCmd = function(user, msg, _meta){
|
||||
if(user.account.effectiveRank >= 256){
|
||||
var fmsg = XSS.sanitizeText(msg).split(" ");
|
||||
fmsg.shift();
|
||||
this.tokesay(fmsg.join(' '), true);
|
||||
}
|
||||
}
|
||||
|
||||
TokebotModule.prototype.tokeyellCmd = function(user, msg, _meta){
|
||||
if(user.account.effectiveRank >= 256){
|
||||
var fmsg = XSS.sanitizeText(msg).split(" ");
|
||||
fmsg.shift();
|
||||
this.tokesay(fmsg.join(' '), false);
|
||||
}
|
||||
}
|
||||
|
||||
TokebotModule.prototype.tokewhisperCmd = function(user, msg, _meta){
|
||||
if(user.account.effectiveRank >= 256){
|
||||
var fmsg = XSS.sanitizeText(msg).split(" ");
|
||||
fmsg.shift();
|
||||
this.tokewhisper(fmsg.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
//extra user commands
|
||||
TokebotModule.prototype.randotoke = function(user, msg, _meta){
|
||||
this.toke(user, '!' + tokes[randi(tokes.length)],_meta);
|
||||
}
|
||||
|
||||
//main toke logic (adapted from chozobot implementation)
|
||||
TokebotModule.prototype.toke = function (user, msg, _meta){
|
||||
var name = user.getName()
|
||||
/*if(name === "Ten"){//use in case of anger
|
||||
bot.sendChatMsg(">:^(");
|
||||
return;
|
||||
}*/
|
||||
switch (this.toking){
|
||||
case 0://ready to start toke
|
||||
this.tokesay("A group toke has been started by " + name + "! We'll be taking a toke in 60 seconds - join in by posting " + msg.split(/\s+/g)[0]);
|
||||
this.cdown = 3;
|
||||
this.toking = 1; this.tokers.push(name);
|
||||
setTimeout(this.countdown, 57000, this);
|
||||
break;
|
||||
case 1://taking toke
|
||||
if(this.tokers.includes(name)){
|
||||
this.tokewhisper(" You're already taking part in this toke!", name);
|
||||
}else{
|
||||
this.tokesay(name + " joined the toke! Post " + msg.split(/\s+/g)[0] + " to take part!");
|
||||
this.tokers.push(name);
|
||||
this.cdown = 3;
|
||||
}
|
||||
break;
|
||||
case 2://cooldown
|
||||
this.tokewhisper(" Please wait " + this.ctime + " seconds before starting a new group toke.", name);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
TokebotModule.prototype.countdown = function (tb){
|
||||
var dateobj = new Date();
|
||||
tb.toking = 1;//set toking mode
|
||||
|
||||
if(dateobj.getUTCMinutes() == 19){
|
||||
var ext = 60 - dateobj.getUTCSeconds();
|
||||
|
||||
if(ext <= 30){//we let it immediatly reset the cooldwon, but might as well extend it
|
||||
console.log("[Tokebot] Toke extended on " + tb.channel.name + " by seconds " + ext + " because it's about to be 4:20 somewhere!");
|
||||
tb.cdown = 3;
|
||||
setTimeout(tb.countdown, 1000 * ext, tb);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tb.tokesay(tb.cdown + "...");//send countdown msg
|
||||
--tb.cdown;//count down
|
||||
|
||||
if(tb.cdown <= 0){//if cdown hits 0
|
||||
setTimeout(tb.endtoke, 1000, tb);
|
||||
}else{
|
||||
setTimeout(tb.countdown, 1000, tb);//call endtoke
|
||||
}
|
||||
};
|
||||
|
||||
TokebotModule.prototype.endtoke = function (tb){
|
||||
if(tb.cdown != 0){
|
||||
setTimeout(tb.countdown, 1000, tb);
|
||||
return;
|
||||
}
|
||||
if(tb.tokers.length > 1){
|
||||
let callstring = '';
|
||||
|
||||
for(let i = 0; i < tb.tokers.length - 1; i++){
|
||||
callstring += tb.tokers[i] + ', ';
|
||||
}
|
||||
|
||||
callstring += tb.tokers[tb.tokers.length - 1];
|
||||
|
||||
tb.tokesay("Take a toke " + callstring + "! " + tb.tokers.length + " tokers!");
|
||||
}else{
|
||||
tb.tokesay("Take a toke " + tb.tokers.toString() + ". " + (solotokes[randi(solotokes.length)]));
|
||||
}
|
||||
tb.logtoke();
|
||||
tb.tokers = [];
|
||||
tb.toking = 2;//reset toking mode
|
||||
setTimeout(tb.cooldown, 1000, tb);
|
||||
};
|
||||
|
||||
TokebotModule.prototype.cooldown = function (tb){
|
||||
if(tb.ctime > 0){
|
||||
var dateobj = new Date();
|
||||
tb.toking = 2;
|
||||
--tb.ctime;
|
||||
if((dateobj.getUTCMinutes() == 19) && (dateobj.getUTCSeconds() >= 30)){
|
||||
console.log("[Tokebot] Cooldown reset on " + tb.channel.name + " triggered because it's about to be 4:20 somewhere!");
|
||||
tb.toking = 0;
|
||||
tb.ctime = tb.cdel;
|
||||
return;
|
||||
}
|
||||
setTimeout(tb.cooldown, 1000, tb);
|
||||
}else{
|
||||
tb.toking = 0;
|
||||
tb.ctime = tb.cdel;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
//helper functions(mostly just syntactic sugar)
|
||||
TokebotModule.prototype.updatetokes = function (){
|
||||
tokes = loadTokes();
|
||||
|
||||
if(this.channel.modules.chat){//register !toke commands
|
||||
if(tokes == null){//if for some reason tokes file couldnt be loaded this.channel.modules.chat.registerCommand("!toke", this.toke.bind(this)); console.log("[tokebot] Unable to load toke commands from ./tokebot/tokes, defaulting to !toke definition");
|
||||
}else{//if we we're able to pull toke commands
|
||||
var _this = this;//we need to use this, might put this up higher to replace the tb parameter in other member functions
|
||||
tokes.forEach(function(tokec){
|
||||
_this.channel.modules.chat.registerCommand("!" + tokec, _this.toke.bind(_this));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TokebotModule.prototype.tokesay = function (msg,quiet){
|
||||
var msgobj = {
|
||||
username: "tokebot",
|
||||
msg: msg,
|
||||
meta:{
|
||||
addClass: (quiet ? null : "shout"),
|
||||
addClassToNameAndTimestamp: true,
|
||||
forceShowName: (quiet ? true : false), //It's loud enough when announcing. Toke chats are rare enough to be more prominent :P
|
||||
modflair: 3
|
||||
},
|
||||
time: Date.now()
|
||||
}
|
||||
|
||||
this.channel.users.forEach(function (u) {
|
||||
u.socket.emit("chatMsg",msgobj);
|
||||
});
|
||||
};
|
||||
|
||||
TokebotModule.prototype.tokewhisper = function (msg, usr){//(msg, username)
|
||||
if(this.channel.modules.chat != null){
|
||||
if(usr != null){
|
||||
this.channel.modules.chat.sendModMessage(msg,-1,"tokebot",usr);
|
||||
}else{
|
||||
var _this = this
|
||||
this.channel.users.forEach(function(u){
|
||||
_this.channel.modules.chat.sendModMessage(msg,-1,"tokebot",u.account.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//filesystem manipulation functions
|
||||
TokebotModule.prototype.writetokelog = function(){//append a toke to current channels toke log
|
||||
var _this = this;
|
||||
fs.appendFile("tokebot/" + this.channel.name + "_toke.log" ,('[' + this.tokers.toString() + '],' + this.tokers.length + ',' + new Date().getTime() + "\n"), function(err){
|
||||
if(err){
|
||||
console.log("[chan: " + _this.channel.name + "] TOKE LOG WRITE ERROR: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TokebotModule.prototype.loadtfile = function(){//load tokefile into statmap(default to new Map() on error)
|
||||
var _this = this;
|
||||
fs.readFile("tokebot/" + this.channel.name + "_tokefile", function(err,data){
|
||||
if(err){
|
||||
console.log("[chan: " + _this.channel.name + "] TOKE FILE READ ERROR (may overwrite existing stat file!): " + err);
|
||||
_this.statmap = new Map();
|
||||
}else{
|
||||
_this.statmap = JSON.parse(data, mapReviver);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TokebotModule.prototype.writetfile = function(){//write statmap to tokefile
|
||||
var _this = this;
|
||||
fs.writeFile("tokebot/" + this.channel.name + "_tokefile", JSON.stringify(_this.statmap, mapReplacer), function(err,data){
|
||||
if(err){
|
||||
console.log("[chan: " + _this.channel.name + "] TOKE FILE WRITE ERROR: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TokebotModule.prototype.logtoke = function(){
|
||||
this.writetokelog();//save toke to toke log
|
||||
var _this = this;
|
||||
|
||||
this.tokers.forEach(function (u){//for all tokers
|
||||
var ct = _this.statmap.get(u);//lets make this a lil prettier :P
|
||||
_this.statmap.set(u,(ct == null ? 1 : ++ct));//increment toke counter
|
||||
});
|
||||
|
||||
this.writetfile();//write statmap to tfile
|
||||
}
|
||||
module.exports = TokebotModule;
|
||||
|
|
@ -65,6 +65,9 @@ var defaults = {
|
|||
"allowed-origins": []
|
||||
}
|
||||
},
|
||||
"invidious-backend": true,
|
||||
"invidious-source": 'inv.riverside.rocks',
|
||||
"yt-dlp-path": 'yt-dlp',
|
||||
"youtube-v3-key": "",
|
||||
"channel-blacklist": [],
|
||||
"channel-path": "r",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ var path = require("path");
|
|||
|
||||
import { callOnce } from './util/call-once';
|
||||
|
||||
const CYTUBE_VERSION = require('../package.json').version;
|
||||
const FOREST_VERSION = require('../package.json').version;
|
||||
|
||||
const LOGGER = require('@calzoneman/jsli')('ffmpeg');
|
||||
const ECODE_MESSAGES = {
|
||||
|
|
@ -181,7 +181,7 @@ function testUrl(url, cb, params = { redirCount: 0, cookie: '' }) {
|
|||
var transport = (data.protocol === "https:") ? https : http;
|
||||
data.method = "HEAD";
|
||||
data.headers = {
|
||||
'User-Agent': `CyTube/${CYTUBE_VERSION}`
|
||||
'User-Agent': `CyTube/${FOREST_VERSION}`
|
||||
};
|
||||
if (cookie) {
|
||||
data.headers['Cookie'] = cookie;
|
||||
|
|
|
|||
356
src/get-info.js
356
src/get-info.js
|
|
@ -1,7 +1,47 @@
|
|||
/*
|
||||
fore.st is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
fore.st is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with fore.st. If not, see < http://www.gnu.org/licenses/ >.
|
||||
(C) 2022- by rainbownapkin, <ourforest@420blaze.it>
|
||||
|
||||
Original cytube license:
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2022 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
const https = require("https");
|
||||
const Media = require("./media");
|
||||
const CustomEmbedFilter = require("./customembed").filter;
|
||||
const Config = require("./config");
|
||||
const XSS = require("./xss");
|
||||
const ffmpeg = require("./ffmpeg");
|
||||
const mediaquery = require("@cytube/mediaquery");
|
||||
const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
|
||||
|
|
@ -9,9 +49,14 @@ const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
|
|||
const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
|
||||
const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
|
||||
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
|
||||
const { create: makeYTDLP } = require('youtube-dl-exec')
|
||||
//Specify path assuming yt-dlp is installed locally with path set properly (version packaged w/ npm package doesn't behave)
|
||||
const YTDLP = makeYTDLP(Config.get('yt-dlp-path'));
|
||||
|
||||
import { Counter } from 'prom-client';
|
||||
import { lookup as lookupCustomMetadata } from './custom-media';
|
||||
|
||||
|
||||
const LOGGER = require('@calzoneman/jsli')('get-info');
|
||||
const lookupCounter = new Counter({
|
||||
name: 'cytube_media_lookups_total',
|
||||
|
|
@ -57,20 +102,36 @@ function convertMedia(media) {
|
|||
media.meta);
|
||||
}
|
||||
|
||||
function getBlocked(reg){
|
||||
var regionlist = ["AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CU","CV","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","GB","GD","GE","GF","GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","ST","SV","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","YE","YT","ZA","ZM","ZW"];//forgive me father, for I have sinned.
|
||||
|
||||
var blck = [];
|
||||
for(var i = 0; i < regionlist.length; i++){
|
||||
if(!reg.includes(regionlist[i])){
|
||||
blck.push(regionlist[i]);
|
||||
}
|
||||
}
|
||||
return blck;
|
||||
}
|
||||
|
||||
var Getters = {
|
||||
/* youtube.com */
|
||||
yt: function (id, callback) {
|
||||
yt: async function (id, callback) {
|
||||
|
||||
if(!Config.get("invidious-backend")){//legacy youtube backend (fucking yicky)
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
return callback("The YouTube API now requires an API key. You could sign up for an API key, but you're a lot better off using the invidious backend!" +
|
||||
"See your config.yaml for the deets.");
|
||||
}
|
||||
|
||||
|
||||
YouTube.lookup(id).then(function (video) {
|
||||
var meta = {};
|
||||
|
||||
if (video.meta.blocked) {
|
||||
meta.restricted = video.meta.blocked;
|
||||
}
|
||||
|
||||
if (video.meta.ytRating) {
|
||||
meta.ytRating = video.meta.ytRating;
|
||||
}
|
||||
|
|
@ -80,13 +141,67 @@ var Getters = {
|
|||
}).catch(function (err) {
|
||||
callback(err.message || err, null);
|
||||
});
|
||||
}else{
|
||||
//yt-dlp calls (google bad)
|
||||
try{
|
||||
var video = await YTDLP(`youtu.be/${id}`,{
|
||||
dumpSingleJson: true,
|
||||
format: "b"
|
||||
}
|
||||
)
|
||||
|
||||
var meta = {
|
||||
ytRating: video.like_count,
|
||||
rawLink: video.requested_downloads[0].url
|
||||
}
|
||||
|
||||
var media = new Media(video.id, video.title, video.duration, "yt", meta);
|
||||
|
||||
|
||||
callback(false, media);
|
||||
}catch(err){
|
||||
callback(err.message || err, null);
|
||||
}
|
||||
|
||||
//callback("test funciton, remove this call!" || err, null);
|
||||
//invidious api calls (o7 Stream on, you magnificent bastard. We'll always remember you! <3 2018-2024)
|
||||
/*var options = {
|
||||
host: Config.get("invidious-source"),
|
||||
port: 443,
|
||||
path: "/api/v1/videos/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
return callback("Invidious HTTPS error code: " + status, null);
|
||||
}
|
||||
|
||||
var vid = JSON.parse(data);
|
||||
var meta = {}
|
||||
|
||||
if(getBlocked(vid.allowedRegions).length > 0){
|
||||
meta.restricted = getBlocked(vid.allowedRegions);
|
||||
}
|
||||
|
||||
if(vid.likeCount){
|
||||
meta.ytRating = vid.likeCount;
|
||||
}
|
||||
|
||||
var media = new Media(vid.videoId, vid.title, vid.lengthSeconds, "yt", meta);
|
||||
return callback(false, media);
|
||||
});*/
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/* youtube.com playlists */
|
||||
yp: function (id, callback) {
|
||||
yp: async function (id, callback) {
|
||||
if(!Config.get("invidious-backend")){//legacy youtube backend (fucking yicky)
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
return callback("The YouTube API now requires an API key. You could sign up for an API key, but you're a lot better off using the invidious backend!" +
|
||||
"See your config.yaml for the deets.");
|
||||
}
|
||||
|
||||
YouTube.lookupPlaylist(id).then(function (videos) {
|
||||
|
|
@ -103,10 +218,63 @@ var Getters = {
|
|||
}).catch(function (err) {
|
||||
callback(err.message || err, null);
|
||||
});
|
||||
}else{//yt-dlp wrapper calls (google bad)
|
||||
try{
|
||||
var list = await YTDLP(`youtu.be/${id}`,{
|
||||
dumpSingleJson: true,
|
||||
format: "b"
|
||||
}
|
||||
);
|
||||
|
||||
var videos = new Array;
|
||||
|
||||
list.entries.forEach(function(video){
|
||||
if(video != null){
|
||||
var meta = {
|
||||
ytRating: video.like_count,
|
||||
rawLink: video.requested_downloads[0].url
|
||||
}
|
||||
|
||||
videos.push(new Media(video.id, video.title, video.duration, "yt", meta));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
callback(null, videos);
|
||||
}catch(err){
|
||||
callback(err.message || err, null);
|
||||
}
|
||||
|
||||
|
||||
/*invidious api calls (o7 RIP 2018-2024)
|
||||
var options = {
|
||||
host: Config.get("invidious-source"),
|
||||
port: 443,
|
||||
path: "/api/v1/playlists/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
return callback("Invidious HTTPS error code: " + status, null);
|
||||
}
|
||||
|
||||
var pl = JSON.parse(data).videos;
|
||||
|
||||
var vids = pl.map(function(vid){
|
||||
return new Media(vid.videoId, vid.title, vid.lengthSeconds, "yt", []);//return the vid as media obj, (skip out on meta to avoid extra api calls)
|
||||
});
|
||||
|
||||
return callback(null, vids);
|
||||
});*/
|
||||
}
|
||||
},
|
||||
|
||||
/* youtube.com search */
|
||||
ytSearch: function (query, callback) {
|
||||
ytSearch: async function (query, callback) {
|
||||
|
||||
if(!Config.get("invidious-backend")){//legacy youtube backend (fucking yicky)
|
||||
if (!Config.get("youtube-v3-key")) {
|
||||
return callback("The YouTube API now requires an API key. Please see the " +
|
||||
"documentation for youtube-v3-key in config.template.yaml");
|
||||
|
|
@ -129,6 +297,59 @@ var Getters = {
|
|||
}).catch(function (err) {
|
||||
callback(err.message || err, null);
|
||||
});
|
||||
}else{
|
||||
|
||||
try{
|
||||
var results = await YTDLP(`ytsearch8:${query}`,{
|
||||
dumpSingleJson: true,
|
||||
}
|
||||
);
|
||||
|
||||
var videos = new Array;
|
||||
|
||||
results.entries.forEach(function(result){
|
||||
var meta = {
|
||||
ytRating: result.like_count
|
||||
}
|
||||
|
||||
var video = new Media(result.id, result.title, result.duration, "yt", meta);
|
||||
video.thumb = {url: result.thumbnails[5].url};
|
||||
|
||||
videos.push(video);
|
||||
});
|
||||
|
||||
callback(null, videos);
|
||||
}catch(err){
|
||||
callback(err.message || err, null);
|
||||
}
|
||||
|
||||
/*invidious api calls (o7 RIP 2018-2024)
|
||||
var options = {
|
||||
host: Config.get("invidious-source"),
|
||||
port: 443,
|
||||
path: "/api/v1/search?q='" + encodeURI(XSS.sanitizeText(query)) + "'",
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
return callback("Invidious HTTPS error code: " + status, null);
|
||||
}
|
||||
|
||||
var srch = JSON.parse(data);
|
||||
var vids = srch.map(function(rslt, i){
|
||||
var media;
|
||||
if(rslt.type === "video"){
|
||||
media = new Media(rslt.videoId, rslt.title, rslt.lengthSeconds, "yt", [])//create new media object from curent rslt
|
||||
media.thumb = {url: rslt.videoThumbnails[5].url};
|
||||
return media
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null, vids.filter(rs => rs != null));
|
||||
});*/
|
||||
}
|
||||
},
|
||||
|
||||
/* vimeo.com */
|
||||
|
|
@ -150,6 +371,8 @@ var Getters = {
|
|||
},
|
||||
|
||||
/* dailymotion.com */
|
||||
//The dailymotion player has been broken, however their basic API remains intact.
|
||||
//This will stay *for now* but will be replaced by yt-dlp at the first sign of resistance.
|
||||
dm: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
|
|
@ -445,18 +668,135 @@ var Getters = {
|
|||
callback,
|
||||
"As of July 2020, Mixer is no longer in service."
|
||||
);
|
||||
},
|
||||
|
||||
/*Internet Archive Bulk Grabber*/
|
||||
ia: function(id, minDuration, callback){
|
||||
try{
|
||||
//Get metadata on the directory
|
||||
var options = {
|
||||
host: "archive.org",
|
||||
port: 443,
|
||||
path: "/metadata/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
//pull the URL
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
//if we fucked up
|
||||
if(status !== 200) {
|
||||
return callback("Archive.org HTTPS error code: " + status, null);
|
||||
}
|
||||
|
||||
//Parse the dump
|
||||
var dump = JSON.parse(data);
|
||||
|
||||
|
||||
//if we have files
|
||||
if(dump.files != null){
|
||||
var vids = new Array;
|
||||
var derivative = new Array;
|
||||
var media = new Array;
|
||||
|
||||
//sift through files to find .mp4's
|
||||
dump.files.forEach(function(file){
|
||||
//Skip out on videos that dont meet the requested minimum duration
|
||||
if(file.length >= minDuration){
|
||||
//if its a standard .mp4 (either MPEG, or h.264)
|
||||
if(file.format == "h.264" || file.format == "MPEG4"){
|
||||
//add the file to the video array
|
||||
vids.push(file);
|
||||
//if it's been transcoded by archive.org to ensure web-compatibility
|
||||
}else if(file.format == "h.264 IA"){
|
||||
//add the file to the derivative array
|
||||
derivative.push(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//chose derivatives over originals to save on bandwith and ensure web-compatibility, even if they don't always look as nice :P
|
||||
derivative.forEach(function(file){
|
||||
//sift through standard files to find matching originals
|
||||
for(var i = 0; i < vids.length; i++){
|
||||
//if we have a match
|
||||
if(vids[i].name == file.original){
|
||||
//replace the file with the correct derivative
|
||||
vids[i] = file;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Lets try this just using info from IA's api. It would take up way less fucking time, even if we can't fill in everything...
|
||||
vids = vids.map(function (file) {
|
||||
return new Media(`https://${dump.d1}${dump.dir}/${file.name}`, dump.metadata.title, file.length, "fi", {codec: "mov/h264"});
|
||||
});
|
||||
|
||||
process.nextTick(callback, false, vids);
|
||||
|
||||
//if we fucked up some other way
|
||||
}else if(dump.error != null){
|
||||
return callback(`Archive.org error: ${dump.error}`);
|
||||
}else{
|
||||
return callback("Unkown metadata error from archive.org!");
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
callback(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Getters: Getters,
|
||||
getMedia: function (id, type, callback) {
|
||||
getMedia: function (id, type, callback, minDuration) {
|
||||
if (type in this.Getters) {
|
||||
LOGGER.info("Looking up %s:%s", type, id);
|
||||
lookupCounter.labels(type).inc(1, new Date());
|
||||
if(type == "ia"){
|
||||
this.Getters.ia(id, minDuration, callback);
|
||||
}else{
|
||||
this.Getters[type](id, callback);
|
||||
}
|
||||
} else {
|
||||
callback("Unknown media type '" + type + "'", null);
|
||||
}
|
||||
},
|
||||
getRawCopy: async function (id, cb){
|
||||
try{
|
||||
var video = await YTDLP(`youtu.be/${id}`,{
|
||||
dumpSingleJson: true,
|
||||
format: "b"
|
||||
}
|
||||
)
|
||||
|
||||
cb(video.requested_downloads[0].url);
|
||||
}catch(err){
|
||||
console.log(err.message);
|
||||
}
|
||||
/* Invidious Code (o7 RIP 2018-2024)
|
||||
var options = {
|
||||
host: Config.get("invidious-source"),
|
||||
port: 443,
|
||||
path: "/api/v1/videos/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
|
||||
old invidious code (o7 2018 - 2024)
|
||||
if(status !== 200) {
|
||||
console.log("Invidious HTTPS error code: " + status);
|
||||
}
|
||||
|
||||
var vid = JSON.parse(data);
|
||||
|
||||
if(vid.formatStreams[0] != null){//TEMPORARY FOR FRONTEND DEV PURPOSES, PULL LINK AND SET AGAIN WHEN VIDEO QUEUED(shit expires)
|
||||
cb(vid.formatStreams[vid.formatStreams.length - 1].url);
|
||||
}
|
||||
|
||||
});*/
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ function Media(id, title, seconds, type, meta) {
|
|||
this.meta = meta;
|
||||
this.currentTime = 0;
|
||||
this.paused = false;
|
||||
this.isBump = false;
|
||||
this.tokeBump = false;
|
||||
}
|
||||
|
||||
Media.prototype = {
|
||||
|
|
@ -33,6 +35,7 @@ Media.prototype = {
|
|||
duration: this.duration,
|
||||
startTime: this.startTime,
|
||||
type: this.type,
|
||||
isBump: this.isBump,
|
||||
meta: {
|
||||
restricted: this.meta.restricted,
|
||||
codec: this.meta.codec,
|
||||
|
|
@ -41,7 +44,8 @@ Media.prototype = {
|
|||
embed: this.meta.embed,
|
||||
gdrive_subtitles: this.meta.gdrive_subtitles,
|
||||
textTracks: this.meta.textTracks,
|
||||
mixer: this.meta.mixer
|
||||
mixer: this.meta.mixer,
|
||||
rawLink: this.meta.rawLink
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
13
src/user.js
13
src/user.js
|
|
@ -10,6 +10,7 @@ import Logger from './logger';
|
|||
import net from 'net';
|
||||
|
||||
const LOGGER = require('@calzoneman/jsli')('user');
|
||||
const FOREST_VERSION = require('../package.json').version;
|
||||
|
||||
function User(socket, ip, loginInfo) {
|
||||
this.flags = 0;
|
||||
|
|
@ -62,6 +63,18 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(typeof data.cv !== "string"){
|
||||
this.socket.emit("errorMsg", {
|
||||
msg: "Invalid client Version. Close the tab and clear your cache!! \n Server Version: " + FOREST_VERSION
|
||||
});
|
||||
this.kick("Invalid client version. Close the tab and clear your cache!");
|
||||
}else if(data.cv !== FOREST_VERSION){
|
||||
this.socket.emit("errorMsg", {
|
||||
msg: "Outdated/Invalid client version. Close the tab and clear your cache!! \n Server Version: " + FOREST_VERSION + " \n Reported Client Version: " + data.cv
|
||||
});
|
||||
this.kick("Invalid client version. Close the tab and clear your cache!");
|
||||
}
|
||||
|
||||
if (this.inChannel()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,11 @@ block content
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
.col-md-8.col-md-offset-2
|
||||
.aboutText
|
||||
h1 Welcome to ourfore.st!
|
||||
h3 about fore.st/ourfore.st
|
||||
p.
|
||||
fore.st is a fork of cytube built for the TTN community post-shutdown. TTN was a community based streaming service for cannabis enthusiasts. After eight years, the man behind the site went on to greener pastures. In it's place stands this, and many other community efforts such as <a href="https://treez.one/">Treezone</a>, and the community discord. While it may not be the same, we aim to provide a similiar service for the same people. The refrence instance for fore.st is hosted at <a href="https://ourfore.st/">ourfore.st</a>.
|
||||
fore.st is a fork of cytube built for the TTN community post-shutdown. TTN was a community based video & stream embedding/chat site for cannabis enthusiasts. After eight years, the man behind the site went on to greener pastures. In it's place stands this, and many other community efforts such as <a href="https://treez.one/">Treezone</a>, and the community discord. While it may not be the same, we aim to provide a similiar service for the same people. The refrence instance for fore.st is hosted at <a href="https://ourfore.st/">ourfore.st</a>.
|
||||
h3 ourfore.st instance rules
|
||||
ul
|
||||
li
|
||||
|
|
@ -57,4 +56,4 @@ block content
|
|||
| No spamming submit channel or chat
|
||||
p.
|
||||
Comments? Questions? Feature requests? DMCA Notices? <a href="mailto:ourforest@420blaze.it">Email us!</a>
|
||||
h4 fore.st version: Pineapple Express (v1.1-INDEV)
|
||||
h4 fore.st version: Pineapple Express += 3 (v1.1.3)
|
||||
|
|
|
|||
|
|
@ -60,14 +60,14 @@ html(lang="en")
|
|||
section#mainpage
|
||||
.container
|
||||
#motdrow.row
|
||||
#motdwrap.well
|
||||
#motdwrap.well.panelback
|
||||
button#togglemotd.close.pull-right(type="button")
|
||||
span.glyphicon.glyphicon-minus
|
||||
#motd
|
||||
.clear
|
||||
#announcements.row
|
||||
#titles.row
|
||||
p#videowrap-header
|
||||
p#videowrap-header.panelback
|
||||
i#blindvideo.glyphicon.glyphicon-chevron-down.pointer(title="Hide Player")
|
||||
span#currenttitle Nothing Playing
|
||||
span#minicontrol
|
||||
|
|
@ -81,9 +81,9 @@ html(lang="en")
|
|||
span#cinemode.playercont.glyphicon.glyphicon-film.pointer(title="Toggle Cinema Mode")
|
||||
span#lockaspect.playercont.glyphicon.glyphicon-picture.pointer(style="display: none;", title="Lock to Aspect Ratio")
|
||||
span#latchvid.label.label-default.pull-right.pointer(style="display: none;") Sync
|
||||
#chatheader
|
||||
#chatheader.panelback
|
||||
i#blindchat.glyphicon.glyphicon-chevron-down.pointer(title="Hide Chat")
|
||||
span#modflair.label.label-default.pull-right.pointer Name Color
|
||||
span#modflair.label.label-default.pointer Modflair
|
||||
span(style="flex-grow: 2;")
|
||||
span#usercount.pointer Not Connected
|
||||
i#userlisttoggle.glyphicon.glyphicon-chevron-down.pull-left.pointer(title="Show/Hide Userlist")
|
||||
|
|
@ -91,28 +91,30 @@ html(lang="en")
|
|||
#videowrap
|
||||
.embed-responsive.embed-responsive-16by9
|
||||
#ytapiplayer.embed-responsive-item
|
||||
div#subliminaltoke
|
||||
div#st
|
||||
img(src="/img/tokeleaf.png")
|
||||
h3 Take a <a onclick="chatsmack('!toke')">Toke!</a>
|
||||
#chatwrap
|
||||
#chatmain
|
||||
#userlist
|
||||
#userlist.panelback
|
||||
#fpaneldiv.fpanel(style="display: none;")
|
||||
#fptitlediv.fptitlebar.fpanel
|
||||
p#fptitle.fptitlebar.fpanel null
|
||||
p#closefpanel.fptitlebar.fpanel.glyphicon.glyphicon-remove.pointer(onclick="javascript:closeFPanel()", title="Close null panel.")
|
||||
#fpcontdiv.fpcont.fpanel
|
||||
#messagebuffer.linewrap
|
||||
#messagebuffer.linewrap.panelback
|
||||
#chatbar(style="display: flex;")
|
||||
button#pollopenbtn.btn.btn-sm.btn-default.glyphicon.glyphicon-ok.chatbtn(onclick="javascript:panelbtn(fpoll)",style="display: none;", title="Poll")
|
||||
button#prefopenbtn.btn.btn-sm.btn-default.glyphicon.glyphicon-cog.chatbtn(onclick="javascript:panelbtn(fpset)",title="Quick Settings")
|
||||
button#emoteopenbtn.btn.btn-sm.btn-default.chatbtn(onclick="javascript:panelbtn(fpemote)",title="Emotes") ;)
|
||||
button#pollopenbtn.btn.btn-sm.btn-default.glyphicon.glyphicon-ok.chatbtn.panelback(onclick="javascript:panelbtn(fpoll)",style="display: none;", title="Poll")
|
||||
button#prefopenbtn.btn.btn-sm.btn-default.glyphicon.glyphicon-cog.chatbtn.panelback(onclick="javascript:panelbtn(fpset)",title="Quick Settings")
|
||||
button#modopenbtn.btn.btn-sm.btn-default.chatbtn.panelback(onclick="javascript:panelbtn(fpmod)",title="Mod Panel", style="display: none;") MOD
|
||||
button#emoteopenbtn.btn.btn-sm.btn-default.chatbtn.panelback(onclick="javascript:panelbtn(fpemote)",title="Emotes") ;)
|
||||
form(action="javascript:void(0)" style="display: flex; flex-grow: 1;")
|
||||
input#chatline.form-control(type="text", maxlength="320", style="display: none")
|
||||
p#chathint
|
||||
input#chatline.form-control.panelback(type="text", maxlength="320", style="display: none")
|
||||
#guestlogin.input-group
|
||||
span.input-group-addon Registration Required!
|
||||
//input#guestname.form-control(type="text", placeholder="Name")
|
||||
button#chatsend.btn.btn-sm.btn-default Send
|
||||
button#chatsend.btn.btn-sm.btn-default.panelback Send
|
||||
|
||||
//#videocontrols.btn-group.pull-right
|
||||
//button#fullscreenbtn.btn.btn-sm.btn-default(title="Make the video player fullscreen")This makes no sense, all supported players already have a full screen button. Not a fan of the placement of this anywho
|
||||
|
|
@ -123,20 +125,22 @@ html(lang="en")
|
|||
//#leftcontrols.col-lg-5.col-md-5
|
||||
//button#newpollbtn.btn.btn-sm.btn-default New Poll
|
||||
#playlistrow.row
|
||||
#rightpane
|
||||
#rightpane.panelback
|
||||
#rightpane-inner.row
|
||||
#rightcontrols
|
||||
#rightcontrols.panelback
|
||||
#plcontrol.btn-group
|
||||
button#showmediaurl.btn.btn-sm.btn-default(title="Add video from URL", data-toggle="collapse", data-target="#addfromurl")
|
||||
span.glyphicon.glyphicon-plus
|
||||
button#golive.btn.btn-sm.btn-danger(title="Go Live")
|
||||
span.glyphicon.glyphicon-record
|
||||
button#showsearch.btn.btn-sm.btn-default(title="Channel History + Video Search", data-toggle="collapse", data-target="#searchcontrol")
|
||||
span.glyphicon.glyphicon-search
|
||||
button#showplaylistmanager.btn.btn-sm.btn-default(title="Manage playlists", data-toggle="collapse", data-target="#playlistmanager")
|
||||
span.glyphicon.glyphicon-list
|
||||
button#scrollitm.btn.btn-sm.btn-default(title="Scroll to Current Item",onclick="javascript:scrollQueue()")
|
||||
button#scrollitm.btn.btn-sm.btn-default(title="Scroll to Current Item",onclick="javascript:scrollQueue(true)")
|
||||
span.glyphicon.glyphicon-hand-right
|
||||
button#getplaylist.btn.btn-sm.btn-default(title="Retrieve playlist links")
|
||||
span.glyphicon.glyphicon-link
|
||||
button#getplaylist.btn.btn-sm.btn-default(title="Export Queue")
|
||||
span.glyphicon.glyphicon-export
|
||||
button#shuffleplaylist.btn.btn-sm.btn-default(title="Shuffle the playlist")
|
||||
span.glyphicon.glyphicon-sort
|
||||
button#clearplaylist.btn.btn-sm.btn-default(title="Clear the playlist")
|
||||
|
|
@ -150,6 +154,7 @@ html(lang="en")
|
|||
span#plcount 0 items
|
||||
br
|
||||
span#pllength 00:00:00
|
||||
#plmenus
|
||||
#searchcontrol.collapse.plcontrol-collapse.col-lg-12.col-md-12
|
||||
.vertical-spacer
|
||||
.input-group
|
||||
|
|
@ -229,6 +234,7 @@ html(lang="en")
|
|||
li: a(href="#us-chat", data-toggle="tab") Chat
|
||||
li: a(href="#us-scriptcontrol", data-toggle="tab") Script Access
|
||||
li: a(href="#us-mod", data-toggle="tab", style="") Moderator
|
||||
li: a(href="#us-access", data-toggle="tab", style="") Accessibility
|
||||
.modal-body
|
||||
.tab-content
|
||||
include useroptions
|
||||
|
|
@ -237,6 +243,7 @@ html(lang="en")
|
|||
+us-chat()
|
||||
+us-scripts()
|
||||
+us-mod()
|
||||
+us-access()
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
|
||||
button.btn.btn-default(type="button", data-dismiss="modal") Close
|
||||
|
|
@ -305,6 +312,7 @@ html(lang="en")
|
|||
script(src="/js/data.js")
|
||||
script(src="/js/fembed.js")
|
||||
script(src="/js/fchat.js")
|
||||
script(src="/js/fccmd.js")
|
||||
script(src="/js/util.js")
|
||||
script(src="/js/tabcomplete.js")
|
||||
script(src="/js/player.js")
|
||||
|
|
@ -317,9 +325,9 @@ html(lang="en")
|
|||
script(defer, src="https://player.vimeo.com/api/player.js")
|
||||
script(defer, src="/js/sc.js")
|
||||
script(defer, src="/js/video.js")
|
||||
script(defer, src="/js/playerjs-0.0.12.js")
|
||||
script(defer, src="/js/videojs-contrib-hls.min.js")
|
||||
script(defer, src="/js/videojs-resolution-switcher.js")
|
||||
script(defer, src="/js/playerjs-0.0.12.js")
|
||||
script(defer, src="/js/dash.all.min.js")
|
||||
script(defer, src="/js/videojs-dash.js")
|
||||
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
extends layout.pug
|
||||
|
||||
block content
|
||||
.col-md-8.col-md-offset-2
|
||||
h1 Google Drive Userscript
|
||||
h2 Disclaimer
|
||||
.alert.alert-danger.messagebox
|
||||
strong Unsupported
|
||||
p.
|
||||
This functionality is provided <strong>as-is</strong> for backwards
|
||||
compatibility for existing users for whom it already is known to work.
|
||||
There are many reasons, known and unknown, for which it may
|
||||
<strong>not</strong> work for you; please note the staff in CyTube
|
||||
support channels cannot provide any troubleshooting assistance and you
|
||||
will be asked to simply use a different video provider.
|
||||
p.
|
||||
This functionality was originally added so that users could share their
|
||||
own personal videos stored in their Drive. No support whatsoever will
|
||||
be provided to users attempting to use it to circumvent copyright
|
||||
restrictions on third-party video hosts.
|
||||
h2 How It Works
|
||||
p.
|
||||
The userscript is a short script that you can install using a browser
|
||||
extension such as Greasemonkey or Tampermonkey that runs on the page
|
||||
and provides additional functionality needed to play Google Drive
|
||||
videos.
|
||||
h2 Installation
|
||||
ul
|
||||
li
|
||||
strong Chrome
|
||||
| —Install <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank">Tampermonkey</a>.
|
||||
li
|
||||
strong Firefox
|
||||
| —Install <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/" target="_blank">Tampermonkey</a>
|
||||
| or <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/" target="_blank">Greasemonkey</a>.
|
||||
li
|
||||
strong Other Browsers
|
||||
| —Install the appropriate userscript plugin for your browser.
|
||||
| Tampermonkey supports many browsers besides Chrome.
|
||||
p.
|
||||
Once you have installed the userscript manager addon for your browser,
|
||||
you can <a href="/js/cytube-google-drive.user.js" target="_blank">
|
||||
install the userscript</a>. If this link 404s, it means the administrator
|
||||
of this server hasn't generated it yet.
|
||||
p.
|
||||
You can find a guide with screenshots of the installation process
|
||||
<a href="https://github.com/calzoneman/sync/wiki/Google-Drive-Userscript-Installation-Guide" target="_blank">on GitHub</a>.
|
||||
|
|
@ -39,7 +39,7 @@ block content
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
.col-lg-9.col-md-9
|
||||
.channel-list
|
||||
h3 Public Channels
|
||||
table.table.table-bordered.table-striped
|
||||
thead
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ mixin lcheckbox(id, label)
|
|||
.checkbox
|
||||
input(type="checkbox", id=id)
|
||||
|
||||
mixin rcheckbox(id, label)
|
||||
mixin rcheckbox(id, label, title)
|
||||
.form-group
|
||||
.col-sm-8.col-sm-offset-4
|
||||
.checkbox
|
||||
label(for=id)
|
||||
input(type="checkbox", id=id)
|
||||
label(for=id, title=title)
|
||||
input(type="checkbox", id=id,title=title)
|
||||
= label
|
||||
|
||||
mixin textbox(id, label, placeholder)
|
||||
|
|
@ -30,12 +30,22 @@ mixin us-general
|
|||
label.control-label.col-sm-4(for="#us-theme") Theme
|
||||
.col-sm-8
|
||||
select#us-theme.form-control
|
||||
option(value="/css/themes/fore.st.css") fore.st
|
||||
option(value="/css/themes/fore.st.css") Dawn [Frosted Glass]
|
||||
option(value="/css/themes/fore.st.lite.css") Dawn [Smoked Quartz]
|
||||
option(value="/css/themes/fore.st.dusk.css") Dusk [Frosted Glass]
|
||||
option(value="/css/themes/fore.st.dusk.lite.css") Dusk [Smoked Quartz]
|
||||
option(value="/css/themes/fore.st.neon.css") Neon [Frosted Glass]
|
||||
option(value="/css/themes/fore.st.neon.lite.css") Neon [Smoked Quartz]
|
||||
option(disabled="true") ---Holiday Themes---
|
||||
option(value="/css/themes/fore.st.candycorn.css") Candycorn [Frosted Glass]
|
||||
option(value="/css/themes/fore.st.candycorn.lite.css") Candycorn [Smoked Quartz]
|
||||
option(value="/css/themes/fore.st.mistletoe.css") Mistletoe [Frosted Glass]
|
||||
option(value="/css/themes/fore.st.mistletoe.lite.css") Mistletoe [Smoked Quartz]
|
||||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-danger Changing layouts may require refreshing to take effect.
|
||||
+rcheckbox("us-no-channelcss", "Ignore Channel CSS")
|
||||
+rcheckbox("us-no-channeljs", "Ignore Channel Javascript")
|
||||
+textbox("us-chat-min", "Chat Width Minimum (Locked to Aspect only.)", "30")
|
||||
.clear
|
||||
|
||||
mixin us-scripts
|
||||
|
|
@ -55,19 +65,21 @@ mixin us-playback
|
|||
form.form-horizontal(action="javascript:void(0)")
|
||||
+rcheckbox("us-synch", "Synchronize video playback")
|
||||
+textbox("us-synch-accuracy", "Synch threshold (seconds)", "2")
|
||||
+rcheckbox("us-wmode-transparent", "Set wmode=transparent")
|
||||
.form-group
|
||||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-info Setting <code>wmode=transparent</code> allows objects to be displayed above the video player, but may cause performance issues on some systems.
|
||||
+rcheckbox("us-wmode-transparent", "Set wmode=transparent", "Allows elements to be placed over the video. May cause a bit of lag on toasters.")
|
||||
+rcheckbox("us-hidevideo", "Remove the video player")
|
||||
+rcheckbox("us-playlistbuttons", "Hide playlist buttons by default")
|
||||
+rcheckbox("us-oldbtns", "Compact playlist buttons")
|
||||
+rcheckbox("us-video-orientation", "Show video orientation buttons above player")
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for="#us-default-quality") Quality Preference
|
||||
label.control-label.col-sm-4(for="#us-default-quality",title="Will not work automagically on official YT embeds because google are dicks.") Quality Preference
|
||||
.col-sm-8
|
||||
select#us-default-quality.form-control
|
||||
select#us-yt-source.form-control(title="YT Embed may not work with all site features.")
|
||||
option(value="vid.puffyan.us") Raw File Link(720p)
|
||||
option(value="OFYT") Official YT Embed
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for="#us-default-quality",title="Will not work automagically on official YT embeds because google are dicks.") Quality Preference
|
||||
.col-sm-8
|
||||
select#us-default-quality.form-control(title="Will not work automagically on officialy YT embeds because google are dicks.")
|
||||
option(value="auto") Auto
|
||||
option(value="240") 240p
|
||||
option(value="360") 360p
|
||||
|
|
@ -75,10 +87,6 @@ mixin us-playback
|
|||
option(value="720") 720p
|
||||
option(value="1080") 1080p
|
||||
option(value="best") Highest Available
|
||||
.form-group
|
||||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-info Due to technical changes on YouTube's side, the CyTube quality preference can no longer be automatically applied on YouTube videos. See <a href="https://github.com/calzoneman/sync/issues/726" rel="noopener noreferer" target="_blank">this GitHub issue</a> for details.
|
||||
|
||||
mixin us-chat
|
||||
#us-chat.tab-pane
|
||||
|
|
@ -89,27 +97,26 @@ mixin us-chat
|
|||
+rcheckbox("us-sort-rank", "Sort userlist by rank")
|
||||
+rcheckbox("us-sort-afk", "Sort AFKers to bottom")
|
||||
+rcheckbox("us-legacy-emote", "Use legacy Cytube emote menu")
|
||||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-info The following 3 options apply to how and when you will be notified if a new chat message is received while CyTube is not the active window.
|
||||
+rcheckbox("us-toke-pm", "Legacy Tokebot Notifications (PM)")
|
||||
+textbox("us-whisper-clear", "Clear whisper & join/leave messages (set 0 to disable)", "10")
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for="#us-blink-title") Blink page title on new messages
|
||||
label.control-label.col-sm-4(for="#us-blink-title",title="Only applies when not active window/tab.") Blink page title on new messages
|
||||
.col-sm-8
|
||||
select#us-blink-title.form-control
|
||||
select#us-blink-title.form-control(title="Only applies when not active window/tab.")
|
||||
option(value="never") Never
|
||||
option(value="onlyping") Only when I am mentioned or PMed
|
||||
option(value="always") Always
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for="#us-ping-sound") Notification sound on new messages
|
||||
label.control-label.col-sm-4(for="#us-ping-sound",title="Only applies when not active window/tab.") Notification sound on new messages
|
||||
.col-sm-8
|
||||
select#us-ping-sound.form-control
|
||||
select#us-ping-sound.form-control(title="Only applies when not active window/tab.")
|
||||
option(value="never") Never
|
||||
option(value="onlyping") Only when I am mentioned or PMed
|
||||
option(value="always") Always
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for="#us-notifications") Desktop notifications on new messages
|
||||
label.control-label.col-sm-4(for="#us-notifications",title="Only applies when not active window/tab.") Desktop notifications on new messages
|
||||
.col-sm-8
|
||||
select#us-notifications.form-control
|
||||
select#us-notifications.form-control(title="Only applies when not active window/tab.")
|
||||
option(value="never") Never
|
||||
option(value="onlyping") Only when I am mentioned or PMed
|
||||
option(value="always") Always
|
||||
|
|
@ -128,3 +135,12 @@ mixin us-mod
|
|||
+rcheckbox("us-modflair", "Show name color")
|
||||
+rcheckbox("us-shadowchat", "Show shadowmuted messages")
|
||||
+rcheckbox("us-show-ip-in-tooltip", "Show IP addresses in profile tooltip")
|
||||
+rcheckbox("us-show-playlist", "Legacy Playlist")
|
||||
+rcheckbox("us-scroll-playlist", "Scroll playlist on change.")
|
||||
+rcheckbox("us-add-invid", "Queue Invidious links as YT links (may cause issues when queueing raw files).")
|
||||
|
||||
mixin us-access
|
||||
#us-access.tab-pane
|
||||
h4 Accessiblity Preferences
|
||||
form.form-horizontal(action="javascript:void(0)")
|
||||
+rcheckbox("us-no-lightning", "Disable Lightning")
|
||||
|
|
|
|||
335
tokebot/tokes
Normal file
335
tokebot/tokes
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
toak
|
||||
666
|
||||
420
|
||||
toke
|
||||
tokem
|
||||
toek
|
||||
hailsatan
|
||||
cheers
|
||||
toast
|
||||
toastem
|
||||
burn
|
||||
burnem
|
||||
lightem
|
||||
dab
|
||||
dabem
|
||||
smoke
|
||||
smokem
|
||||
blaze
|
||||
blazeit
|
||||
blazem
|
||||
drink
|
||||
shot
|
||||
weed
|
||||
marijuana
|
||||
cannabis
|
||||
jazzcabbage
|
||||
oktem
|
||||
puff
|
||||
hit
|
||||
tjoke
|
||||
tjokem
|
||||
devilslettuce
|
||||
toakem
|
||||
grass
|
||||
liftoff
|
||||
420blazeit
|
||||
smokeweed420blazeit
|
||||
smokeweed420blazem
|
||||
boof
|
||||
boofem
|
||||
tonk
|
||||
tonkem
|
||||
tonker
|
||||
bonghits4jesus
|
||||
tedcruzdid911
|
||||
epsteindidntkillhimself
|
||||
zillatoke
|
||||
ekot
|
||||
mekot
|
||||
smonk
|
||||
smonkem
|
||||
hash
|
||||
kush
|
||||
cheeseit
|
||||
munch
|
||||
munchem
|
||||
vape
|
||||
vapem
|
||||
fire
|
||||
firemup
|
||||
sacrifice
|
||||
710
|
||||
roast
|
||||
nukem
|
||||
shit
|
||||
hydrate
|
||||
eat
|
||||
edible
|
||||
justgirlythings
|
||||
heyrainbowaddthis
|
||||
inhale
|
||||
ignite
|
||||
theplant
|
||||
spark
|
||||
sparkone
|
||||
sparkem
|
||||
smokeweederryday
|
||||
robotoke
|
||||
witness
|
||||
roastem
|
||||
crabpeople
|
||||
shootthemoon
|
||||
ballmastr
|
||||
checkitout
|
||||
brule
|
||||
strangerthings
|
||||
strange
|
||||
silicon
|
||||
goteamventure
|
||||
snaildown
|
||||
high
|
||||
stoned
|
||||
drunk
|
||||
glazzballs
|
||||
sin
|
||||
vibe
|
||||
frittata
|
||||
breen
|
||||
soup
|
||||
robots
|
||||
love
|
||||
hootiehoo
|
||||
pot
|
||||
toe
|
||||
feet
|
||||
foot
|
||||
science
|
||||
horse
|
||||
bagel
|
||||
roach
|
||||
ranch
|
||||
weedistight
|
||||
mattea
|
||||
katebush
|
||||
runningupthathill
|
||||
running
|
||||
morbin
|
||||
yee
|
||||
maidenless
|
||||
smellyplants
|
||||
foryourhealth
|
||||
cromdar
|
||||
cbd
|
||||
yeet
|
||||
viking
|
||||
suck
|
||||
vamp
|
||||
sparkle
|
||||
northman
|
||||
jack
|
||||
keanu
|
||||
woof
|
||||
yote
|
||||
pizza
|
||||
dudewhat
|
||||
wine
|
||||
red
|
||||
beer
|
||||
liquor
|
||||
ipa
|
||||
a
|
||||
c
|
||||
d
|
||||
x
|
||||
y
|
||||
z
|
||||
n
|
||||
rat
|
||||
smeg
|
||||
meg
|
||||
kegels
|
||||
greasy
|
||||
bullshitartist
|
||||
spear
|
||||
fang
|
||||
brutal
|
||||
dethklok
|
||||
goodbye
|
||||
tab
|
||||
jfc
|
||||
ghost
|
||||
burger
|
||||
toad
|
||||
snek
|
||||
pill
|
||||
2count
|
||||
doobie
|
||||
dooby
|
||||
yeehaw
|
||||
debra
|
||||
tree
|
||||
69
|
||||
311
|
||||
buttstonked
|
||||
trees
|
||||
treez
|
||||
plants
|
||||
plantz
|
||||
aliens
|
||||
ollie
|
||||
greatscott
|
||||
121gigawatts
|
||||
88mph
|
||||
smokeweedblazeit
|
||||
smek
|
||||
cry
|
||||
smook
|
||||
justdudethings
|
||||
oscarfever
|
||||
007
|
||||
kiff
|
||||
kief
|
||||
jeff
|
||||
gay
|
||||
lesbian
|
||||
bi
|
||||
trans
|
||||
queer
|
||||
meow
|
||||
tocar
|
||||
fumar
|
||||
ruhroh
|
||||
spoop
|
||||
tok
|
||||
fight
|
||||
club
|
||||
stab
|
||||
otke
|
||||
greatergood
|
||||
toker
|
||||
tokes
|
||||
toked
|
||||
twunk
|
||||
twonk
|
||||
doublerainbow
|
||||
kava
|
||||
kratom
|
||||
catjam
|
||||
fuck
|
||||
squanch
|
||||
snortskie
|
||||
zoinks
|
||||
luckoftheirish
|
||||
comeondown
|
||||
ihaventeventriedityet
|
||||
warter
|
||||
water
|
||||
death
|
||||
dead
|
||||
smokeweed
|
||||
quack
|
||||
clurb
|
||||
coffee
|
||||
duck
|
||||
cum
|
||||
nut
|
||||
bong
|
||||
piss
|
||||
smok
|
||||
basmati
|
||||
meds
|
||||
vitamins
|
||||
protein
|
||||
toasty
|
||||
squirt
|
||||
drinkwater
|
||||
chug
|
||||
olympic
|
||||
marihuana
|
||||
hooray
|
||||
flambe
|
||||
flambé
|
||||
tenturnyourrainsoundsoff
|
||||
jabroni
|
||||
lame
|
||||
yoke
|
||||
reefer
|
||||
bloke
|
||||
hailsanta
|
||||
tonks
|
||||
henshin
|
||||
maryjane
|
||||
fart
|
||||
shart
|
||||
jinkies
|
||||
whatastorymark
|
||||
ohhaimark
|
||||
cheep
|
||||
bat
|
||||
bats
|
||||
batman
|
||||
birdgirl
|
||||
himbo
|
||||
t
|
||||
o
|
||||
k
|
||||
e
|
||||
b
|
||||
popcorn
|
||||
puppy
|
||||
puppybowl
|
||||
superbowl
|
||||
super
|
||||
bowl
|
||||
owl
|
||||
kitty
|
||||
crab
|
||||
rso
|
||||
drugs
|
||||
drugz
|
||||
spacesurf
|
||||
birdup
|
||||
eltoke
|
||||
thistooshallpass
|
||||
beber
|
||||
wrasslin
|
||||
her
|
||||
yikes
|
||||
booyak
|
||||
bust
|
||||
bustin
|
||||
weedeven
|
||||
even
|
||||
succ
|
||||
barm
|
||||
propane
|
||||
dope
|
||||
yep
|
||||
dangit
|
||||
bobby
|
||||
dabs
|
||||
mclovin
|
||||
delaware
|
||||
tight
|
||||
doh
|
||||
ass
|
||||
dick
|
||||
scottbaio
|
||||
resin
|
||||
penisman
|
||||
punchy
|
||||
hotto
|
||||
geekedup
|
||||
comedy
|
||||
bake
|
||||
baked
|
||||
shweed
|
||||
kungfu
|
||||
dream
|
||||
MDK
|
||||
3ven
|
||||
SmoothAsEggs
|
||||
nosedive
|
||||
rip
|
||||
slorp
|
||||
BIN
www/audio/thunder0.ogg
Normal file
BIN
www/audio/thunder0.ogg
Normal file
Binary file not shown.
BIN
www/audio/thunder1.ogg
Normal file
BIN
www/audio/thunder1.ogg
Normal file
Binary file not shown.
|
|
@ -40,21 +40,49 @@ body{
|
|||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#mp-show-ip-in-tooltip{
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
#main, #titles{
|
||||
display: flex;
|
||||
}
|
||||
.static-cont{
|
||||
position: absolute;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
}
|
||||
.qt{
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
#qt-clearuser{
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.container-fluid {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.nmenu{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
.nmenu-btn{
|
||||
flex-grow: 1;
|
||||
}
|
||||
#minicontrol{
|
||||
display: none;
|
||||
margin: 0 0.5em 0 0.5em;
|
||||
}
|
||||
|
||||
#vidplay{
|
||||
}
|
||||
#viddur{
|
||||
|
|
@ -63,7 +91,14 @@ body{
|
|||
#loginform > .form-group {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#npoll-timeout{
|
||||
width: 4em;
|
||||
}
|
||||
#npopt{
|
||||
display: flex;
|
||||
height: 1.6em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -98,7 +133,7 @@ body{
|
|||
#messagebuffer, #userlist {
|
||||
height: 329px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +142,12 @@ body{
|
|||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.floatcont{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.linewrap, .linewrap code {
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
|
|
@ -152,6 +192,7 @@ body{
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#chatheader > p, #videowrap-header {
|
||||
|
|
@ -282,7 +323,7 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
|||
font-family: Monospace;
|
||||
}
|
||||
|
||||
.qe_sTime, .qe_pref{
|
||||
.qe_etime, .qe_sTime, .qe_pref{
|
||||
float: right;
|
||||
font-family: Monospace;
|
||||
}
|
||||
|
|
@ -306,7 +347,7 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
|||
}
|
||||
#fpcontdiv{
|
||||
}
|
||||
#subliminaltoke{
|
||||
#st{
|
||||
background-color: #111111C0;
|
||||
position: absolute;
|
||||
left: calc(50% - 12em);
|
||||
|
|
@ -315,8 +356,9 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
|||
padding: 2em 2em 1em 2em;
|
||||
border: 1px solid #949494;
|
||||
display: none;
|
||||
z-index: 10001;
|
||||
}
|
||||
#subliminaltoke img{
|
||||
#st img{
|
||||
max-height: 20em;
|
||||
}
|
||||
.emotecontdiv{
|
||||
|
|
@ -360,10 +402,8 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
|||
}
|
||||
#emotecont{
|
||||
display: flex;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background-color: #1119;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
#esearchbar{
|
||||
border-right: 1px solid #949494;
|
||||
|
|
@ -420,7 +460,8 @@ label[for="emotealphabox"]{
|
|||
font-family: Monospace;
|
||||
}
|
||||
.fpcont{
|
||||
overflow: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#optedit, #permedit, #filteredit, #motdedit, #cssedit, #jsedit,
|
||||
#banlist, #loginhistory, #channelranks, #chanlog {
|
||||
|
|
@ -779,19 +820,68 @@ table td {
|
|||
#userlisttoggle {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.queue_entry {
|
||||
#abumpdiv{
|
||||
overflow-y: auto;
|
||||
}
|
||||
.queue_entry, .ab-bumparray-bump{
|
||||
line-height: 22px;
|
||||
padding: 2px;
|
||||
font-size: 8pt;
|
||||
border: 1px solid;
|
||||
border-top-width: 0;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.ab-bumparray-span{
|
||||
display: grid;
|
||||
grid-template: 2em 2em / 33% 33% auto;
|
||||
grid-auto-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.ab-bumparray-creator{
|
||||
display: inline;
|
||||
}
|
||||
.ab-bumparray-cspan{
|
||||
float: right;
|
||||
}
|
||||
#ab-bumphist-div{
|
||||
overflow-y: auto;
|
||||
max-height: 25em;
|
||||
}
|
||||
.ab-bumplist-lbl, .ab-newlist-form{
|
||||
display: inline;
|
||||
}
|
||||
.ab-newlist-form{
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.ab-newbump-form{
|
||||
display: inline;
|
||||
}
|
||||
.ab-newbump-cancel{
|
||||
display: inline;
|
||||
}
|
||||
.emotelist-table {
|
||||
margin: auto;
|
||||
}
|
||||
#ab-freq-min, #ab-freq-max, #qs-chat-min, #qs-sync-threshold, #qs-whisper-clear{
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#ab-dur-min{
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
.ab-bumplists-ltype, .ab-bumplist-listname{
|
||||
display: inline;
|
||||
}
|
||||
.ab-bumplist-bumps{
|
||||
max-height: 30em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ab-bumplist-delete{
|
||||
float: right;
|
||||
}
|
||||
.emote-preview-container {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
|
@ -895,6 +985,9 @@ input#logout[type="submit"]:hover {
|
|||
font-weight: 700;
|
||||
display: inline;
|
||||
}
|
||||
#ab-bumparray-reuserfl{
|
||||
margin: 0px;
|
||||
}
|
||||
body.hd #resize-video-larger, body.hd #resize-video-smaller {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -902,3 +995,25 @@ body.hd #resize-video-larger, body.hd #resize-video-smaller {
|
|||
.userlist-ignored {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#lightning{
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
height: 99.95%;/*setting both of these to any higher than this completely fucking breaks it on firefox, don't ask why I have no fucking clue. Works fine on chrome if both are 100%*/
|
||||
top: 0;
|
||||
position: fixed;
|
||||
z-index: 10005
|
||||
}
|
||||
|
||||
#chathint{
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
margin-top: 0.64em;
|
||||
margin-left: 0.92em
|
||||
}
|
||||
|
||||
.hintspace{
|
||||
color: rgba(0,0,0,0);
|
||||
background-color: rgba(0,0,0,0);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
|
|
|||
5888
www/css/themes/fore.st.candycorn.css
Normal file
5888
www/css/themes/fore.st.candycorn.css
Normal file
File diff suppressed because it is too large
Load diff
5889
www/css/themes/fore.st.candycorn.lite.css
Normal file
5889
www/css/themes/fore.st.candycorn.lite.css
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
5889
www/css/themes/fore.st.dusk.lite.css
Normal file
5889
www/css/themes/fore.st.dusk.lite.css
Normal file
File diff suppressed because it is too large
Load diff
5888
www/css/themes/fore.st.lite.css
Normal file
5888
www/css/themes/fore.st.lite.css
Normal file
File diff suppressed because it is too large
Load diff
5931
www/css/themes/fore.st.mistletoe.css
Normal file
5931
www/css/themes/fore.st.mistletoe.css
Normal file
File diff suppressed because it is too large
Load diff
5931
www/css/themes/fore.st.mistletoe.lite.css
Normal file
5931
www/css/themes/fore.st.mistletoe.lite.css
Normal file
File diff suppressed because it is too large
Load diff
5888
www/css/themes/fore.st.neon.css
Normal file
5888
www/css/themes/fore.st.neon.css
Normal file
File diff suppressed because it is too large
Load diff
5888
www/css/themes/fore.st.neon.lite.css
Normal file
5888
www/css/themes/fore.st.neon.lite.css
Normal file
File diff suppressed because it is too large
Load diff
BIN
www/img/bumps.png
Normal file
BIN
www/img/bumps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
www/img/bumpsdusk.png
Normal file
BIN
www/img/bumpsdusk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
www/img/frstdusk.png
Normal file
BIN
www/img/frstdusk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 MiB |
BIN
www/img/pumpkin.jpg
Normal file
BIN
www/img/pumpkin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
BIN
www/img/snow.png
Normal file
BIN
www/img/snow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
www/img/xmas.png
Normal file
BIN
www/img/xmas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 MiB |
|
|
@ -44,7 +44,8 @@ Callbacks = {
|
|||
SOCKETIO_CONNECT_ERROR_COUNT = 0;
|
||||
$("#socketio-connect-error").remove();
|
||||
socket.emit("joinChannel", {
|
||||
name: CHANNEL.name
|
||||
name: CHANNEL.name,
|
||||
cv: CL_VERSION
|
||||
});
|
||||
|
||||
if (CHANNEL.opts.password) {
|
||||
|
|
@ -222,14 +223,16 @@ Callbacks = {
|
|||
$("<p/>").appendTo(div)
|
||||
.html("Go watch one of the channels that actually exists <a href='/'>here</a>.");
|
||||
|
||||
$("#ytapiplayer").append('<embed type="text/html" src="https://vid.puffyan.us/embed/HS-xJLNROqE?Autoplay=1&loop=1">')//KSSSSSSSSSSSSSSSSSSSSSSSH
|
||||
$("#ytapiplayer").removeClass("embed-responsive-item").addClass("static-cont");
|
||||
$("#ytapiplayer").append('<video src="' + location.origin +'/vid/static.webm" autoplay loop class="static-cont">');//KSSSSSSSSSSSSSSSSSSSSSSSH
|
||||
handleWindowResize();
|
||||
},
|
||||
|
||||
setMotd: function(motd) {
|
||||
CHANNEL.motd = motd;
|
||||
$("#motd").html(motd);
|
||||
$("#cs-motdtext").val(motd);
|
||||
if (motd != "") {
|
||||
if (motd != "" && !USEROPTS.cinema_mode) {
|
||||
$("#motdwrap").show();
|
||||
$("#motd").show();
|
||||
$("#togglemotd").find(".glyphicon-plus")
|
||||
|
|
@ -510,6 +513,7 @@ Callbacks = {
|
|||
|
||||
/* REGION Chat */
|
||||
usercount: function(count) {
|
||||
count++;//add one fer tokebot :P
|
||||
CHANNEL.usercount = count;
|
||||
var text = count + " connected user";
|
||||
if(count != 1) {
|
||||
|
|
@ -519,6 +523,34 @@ Callbacks = {
|
|||
},
|
||||
|
||||
chatMsg: function(data) {
|
||||
if(data.username === "tokebot"){
|
||||
if(data.meta.addClass === "server-whisper"){
|
||||
data.meta.modflair = 3;
|
||||
if( USEROPTS.toke_pm){
|
||||
data.meta = {
|
||||
modflair: 3
|
||||
};
|
||||
window.Callbacks.pm(data);
|
||||
return;
|
||||
}
|
||||
}else if(data.meta.addClass === "shout" && data.msg.startsWith("Take a toke")){
|
||||
data.msg.split(" ").forEach(function(w){
|
||||
var n = w.slice(0,-1);
|
||||
|
||||
if(usrColors[0].includes(n)){
|
||||
$(".userlist_" + n).data().meta.toke++;
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}else if(data.meta.addClass === "shout" && (data.msg.startsWith("A group toke has been started by " + CLIENT.name + "!") || data.msg.startsWith(CLIENT.name + " joined the toke!"))){
|
||||
data.msg.split(" ").forEach(function(w){
|
||||
if(w.charAt(0) === '!' && w !== "!toke"){
|
||||
storeToke(w);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
addChatMessage(data);
|
||||
},
|
||||
|
||||
|
|
@ -550,8 +582,6 @@ Callbacks = {
|
|||
},
|
||||
|
||||
clearchat: function(data) {
|
||||
console.log(data);
|
||||
|
||||
if(data.target == null){
|
||||
$("#messagebuffer").html("");
|
||||
}else{
|
||||
|
|
@ -718,9 +748,12 @@ Callbacks = {
|
|||
li.attr("title", data[i].queueby
|
||||
? ("Added by: " + data[i].queueby)
|
||||
: "Added by: Unknown");
|
||||
if(data[i].media.type === "fi"){
|
||||
if(data[i].media.type === "fi" && data[i].media.id.includes("expires=")){
|
||||
li.attr("id", "filei");
|
||||
}
|
||||
if(data[i].media.isBump){
|
||||
li.addClass("bumpi");
|
||||
}
|
||||
li.appendTo(q);
|
||||
}
|
||||
|
||||
|
|
@ -745,10 +778,15 @@ Callbacks = {
|
|||
li.addClass("queue_active");
|
||||
activeItem = data.uid;
|
||||
}
|
||||
if(data.item.media.type === "fi"){
|
||||
|
||||
if(data.item.media.type === "fi" && data.item.media.id.includes("expires=")){
|
||||
li.attr("id", "filei");
|
||||
}
|
||||
|
||||
if(data.item.media.isBump){
|
||||
li.addClass("bumpi");
|
||||
}
|
||||
|
||||
li.hide();
|
||||
var q = $("#queue");
|
||||
li.attr("title", data.item.queueby
|
||||
|
|
@ -859,14 +897,16 @@ Callbacks = {
|
|||
if (isNaN(VOLUME) || VOLUME > 1 || VOLUME < 0) {
|
||||
VOLUME = 1;
|
||||
}
|
||||
|
||||
function loadNext() {
|
||||
if(PLAYER){
|
||||
PLAYER.latch();
|
||||
PLAYER.latchseek();
|
||||
}
|
||||
$("#latchvid").hide();
|
||||
if (!PLAYER || data.type !== PLAYER.mediaType) {
|
||||
loadMediaPlayer(data);
|
||||
PLAYER.latch();
|
||||
PLAYER.latchseek();
|
||||
} else {
|
||||
handleMediaUpdate(data);
|
||||
}
|
||||
|
|
@ -1178,7 +1218,95 @@ Callbacks = {
|
|||
if (CHANNEL.opts.allow_voteskip && hasPermission("voteskip")) {
|
||||
$("#voteskip").attr("disabled", false);
|
||||
}
|
||||
},
|
||||
|
||||
sendBumplists: function (data){
|
||||
data.lists.forEach(function(list){//tattoo listnames, saves bandwith
|
||||
list.bumps.forEach(function(bump){
|
||||
if(bump != null){
|
||||
bump.listname = list.lowername;
|
||||
}
|
||||
});
|
||||
});
|
||||
CHANNEL.bumpdata = data;//the best answer is usually the easy one. Why re-create an object client side when we can just use the one we got handed?
|
||||
if(CURRENTFPANEL == fpbump){
|
||||
fpbump.reloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
sendBumplist: function (data){
|
||||
var lfound = null;
|
||||
CHANNEL.bumpdata.lists.forEach(function(list, i){
|
||||
if(data.oname == null){
|
||||
if(lfound == null && data.lowername === list.lowername){
|
||||
lfound = i;
|
||||
}
|
||||
}else{
|
||||
if(lfound == null && data.oname === list.lowername){
|
||||
lfound = i;
|
||||
data.oname = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
data.bumps.forEach(function(bump){
|
||||
if(bump != null){
|
||||
bump.listname = data.lowername;
|
||||
}
|
||||
});
|
||||
|
||||
if(lfound != null){
|
||||
CHANNEL.bumpdata.lists[lfound] = data;
|
||||
}else{
|
||||
CHANNEL.bumpdata.lists.push(data);
|
||||
}
|
||||
|
||||
if(CURRENTFPANEL == fpbump){
|
||||
fpbump.reloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
rmBumplist: function (data){
|
||||
CHANNEL.bumpdata.lists.forEach(function(list, i){
|
||||
if(data === list.lowername){
|
||||
CHANNEL.bumpdata.lists.splice(i, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if(CURRENTFPANEL == fpbump){
|
||||
fpbump.reloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
sendBumpconf: function (data){
|
||||
CHANNEL.bumpdata.active = data.active;
|
||||
CHANNEL.bumpdata.agro = data.agro;
|
||||
CHANNEL.bumpdata.bsort = data.bsort;
|
||||
CHANNEL.bumpdata.freq = data.freq;
|
||||
CHANNEL.bumpdata.lsort = data.lsort;
|
||||
CHANNEL.bumpdata.minBump = data.minBump;
|
||||
|
||||
if(CURRENTFPANEL == fpbump){
|
||||
fpbump.reloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
sendBumphist: function (data){
|
||||
CHANNEL.bumpdata.history = data;
|
||||
|
||||
if(CURRENTFPANEL == fpbump){
|
||||
fpbump.reloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
remoteCmd: function(data){
|
||||
cfunc = commands.get(data.cmd);
|
||||
|
||||
if(typeof cfunc === "function"){
|
||||
cfunc(data.fullc);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
var SOCKET_DEBUG = localStorage.getItem('cytube_socket_debug') === 'true';
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
*/
|
||||
|
||||
var CL_VERSION = 1.1;
|
||||
var CL_VERSION = "1.1.3";
|
||||
var GS_VERSION = 1.7; // Google Drive Userscript
|
||||
|
||||
var CLIENT = {
|
||||
|
|
@ -61,7 +61,8 @@ var CHANNEL = {
|
|||
motd: "",
|
||||
name: CHANNELNAME,
|
||||
usercount: 0,
|
||||
emotes: []
|
||||
emotes: [],
|
||||
bumpdata: {}
|
||||
};
|
||||
|
||||
var PLAYER = false;
|
||||
|
|
@ -170,6 +171,15 @@ var USEROPTS = {
|
|||
sort_rank : getOrDefault("sort_rank", true),
|
||||
sort_afk : getOrDefault("sort_afk", false),
|
||||
legacy_emote : getOrDefault("legacy_emote", false),
|
||||
scroll_list : getOrDefault("scroll_list", false),
|
||||
toke_pm : getOrDefault("toke_pm", false),
|
||||
whisper_clear : getOrDefault("whisper_clear", 10),
|
||||
no_lightning : getOrDefault("no_lightning", false),
|
||||
yt_source : getOrDefault("yt_source", "vid.puffyan.us"),
|
||||
add_invid : getOrDefault("add_invid", true),
|
||||
chat_min : getOrDefault("chat_min", 25),
|
||||
show_userlist : getOrDefault("show_userlist", true),
|
||||
cinema_mode : getOrDefault("cinema_mode", false),
|
||||
show_seconds : getOrDefault("show_seconds", false),
|
||||
default_quality : getOrDefault("default_quality", "auto"),
|
||||
boop : getOrDefault("boop", "never"),
|
||||
|
|
@ -180,6 +190,7 @@ var USEROPTS = {
|
|||
chat_tab_method : getOrDefault("chat_tab_method", "Cycle options"),
|
||||
notifications : getOrDefault("notifications", "never"),
|
||||
show_ip_in_tooltip : getOrDefault("show_ip_in_tooltip", true),
|
||||
show_playlist : getOrDefault("show_playlist", false),
|
||||
show_orientation : getOrDefault("show_orientation", "true")
|
||||
};
|
||||
|
||||
|
|
|
|||
37
www/js/fccmd.js
Normal file
37
www/js/fccmd.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const commands = new Map([
|
||||
["thunder",thunder],
|
||||
["lightning",thunder],
|
||||
]);
|
||||
|
||||
function thunder(){
|
||||
var lfx = $("<div>").attr("id","lightning");
|
||||
var tfx = $("<audio>").append($("<source>").attr("type","audio/ogg").attr("src", window.location.origin + "/audio/thunder" + randrange(0,1) + ".ogg"));
|
||||
|
||||
var lfxinter;
|
||||
var flashind = 0;
|
||||
var tcount = randrange(2,5);
|
||||
|
||||
$("body").append(lfx.hide());
|
||||
$("body").append(tfx.hide());
|
||||
tfx.on("ended",function(){
|
||||
tfx.remove();
|
||||
});
|
||||
|
||||
function showlfx(){
|
||||
if(flashind < tcount){
|
||||
$("#lightning").show("fade", randrange(10,130),function(){
|
||||
$("#lightning").hide("fade", randrange(10,130),function(){
|
||||
setTimeout(showlfx, randrange(15 + (flashind * 25),80 + (flashind * 15)));
|
||||
flashind++;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(!USEROPTS.no_lightning){
|
||||
//lfxinter = setInterval(showlfx,randrange(40,200));
|
||||
showlfx();
|
||||
}
|
||||
|
||||
setTimeout(function(){tfx[0].play()}, randrange(40,60));
|
||||
}
|
||||
|
|
@ -81,6 +81,21 @@ function chatsmack(str){
|
|||
chatline.value = buf;
|
||||
}
|
||||
|
||||
function chathint(rhint){
|
||||
cval = $("#chatline").val();//syntatic sugar :p
|
||||
|
||||
csplit = cval.split(/\s+/);
|
||||
hspace = cval.slice(0,cval.length - csplit[csplit.length - 1].length);
|
||||
|
||||
hint = rhint.slice(cval.length);
|
||||
|
||||
if(hint != null && hint != ""){
|
||||
$("#chathint").html('<span class="hintspace">' + cval + '</span>' + hint);
|
||||
}else{
|
||||
$("#chathint").html("");
|
||||
}
|
||||
}
|
||||
|
||||
Storage.prototype.setObj = function(key, obj) {
|
||||
return this.setItem(key, JSON.stringify(obj))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,21 @@ function checkMedia(fname){//check if link points ot media
|
|||
function checkEmbed(word, isEmote){
|
||||
let regex = /[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g;//symbol mask for username
|
||||
let tregex = /["#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g;//symbol mask for tokes
|
||||
let cregex = /[!"#$%&'()*+,-.:;<=>?@[\]^_`{|}~]/g;//symbol mask for tokes
|
||||
let stripd = word.replace(regex, '');//stripped word for username detection
|
||||
let tstripd = word.replace(tregex, '');//stripeed word for !toke command detection
|
||||
let tstripd = word.replace(tregex, '');//stripped word for !toke command detection
|
||||
let cstripd = word.replace(cregex, '');//stripped word for /local command detection
|
||||
if(word.includes(proto[0]) || word.includes(proto[1])){//check if it starts with a supported proto
|
||||
if(checkMedia(word) != 0){//check if media
|
||||
return '<img src="' + word + '" style="max-height: 13em">';//embed media
|
||||
return '<img src="' + word + '" class="channel-emote">';//embed media
|
||||
}else if(!isEmote){//if its a link
|
||||
if(word.includes("imgur.com")){
|
||||
if(word.length > 20 && word.length < 28){
|
||||
return '<img src="' + word + ".gif" + '" style="max-height: 13em">';//embed media
|
||||
return '<img src="' + word + ".gif" + '" class="channel-emote">';//embed media
|
||||
}
|
||||
}
|
||||
return '<a target="_blank" href="' + word + '">' + word + '</a>';//embed link
|
||||
}else{
|
||||
return '<a target="_blank" href="' + word + '">' + word.replace(/.{40}/g, '$& ') + '</a>';//embed link
|
||||
}else{//if its an emote
|
||||
return word;
|
||||
}
|
||||
}else if(usrColors[0].includes(stripd)){//if username
|
||||
|
|
@ -55,8 +57,11 @@ function checkEmbed(word, isEmote){
|
|||
}else if(tstripd.charAt(0) === '!' && !isEmote){//if !toke command(same logic as above)
|
||||
let tokesplit = word.split(tstripd,2);
|
||||
return tokesplit[0] + '<a id="toke" onclick="chatsmack(\'' + tstripd + '\')">' + tstripd + '</a>' + tokesplit[1];
|
||||
}else if(cstripd.charAt(0) === '/' && !isEmote){//if local command(same logic as above)
|
||||
let cmdsplit = word.split(cstripd,2);
|
||||
return cmdsplit[0] + '<a id="toke" onclick="chatsmack(\'' + cstripd + '\')">' + cstripd + '</a>' + cmdsplit[1];
|
||||
}else{
|
||||
return word;
|
||||
return word.replace(/.{40}/g, '$& ');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
962
www/js/fpanel.js
962
www/js/fpanel.js
File diff suppressed because it is too large
Load diff
|
|
@ -26,11 +26,12 @@ function dispSTimes(){//update sTimes
|
|||
var ptimeString, ltimeString;
|
||||
calcRefs();//iterate and findRefs before calcTime
|
||||
|
||||
//TODO: replace this with foreach
|
||||
//iterate and print
|
||||
for(var i = 0; i < startTimes[0].length; i++){//for every item startTime
|
||||
var rdif = startTimes[1][i] - rptime;
|
||||
st.setTime(rltime + (rdif * 1000));
|
||||
ltimeString = "airdate: " + st.toLocaleTimeString() + " " + st.toLocaleDateString();
|
||||
ltimeString = "airdate: " + st.toLocaleTimeString().replace(" ","") + " " + st.toLocaleDateString();
|
||||
//ptimeString = '<span id="prefTime"> (pref time) ' + formatTime(startTimes[1][i] + '</span>');// create ptimeString
|
||||
ptimeString = '(pref time) ' + formatTime(startTimes[1][i]);// create ptimeString
|
||||
|
||||
|
|
@ -39,6 +40,12 @@ function dispSTimes(){//update sTimes
|
|||
$(".pluid-" + startTimes[0][i]).children(".qe_sTime").text(ltimeString);// set current item qe_sTime innerHTML to ptimeString
|
||||
$(".pluid-" + startTimes[0][i]).children(".qe_pref").text(ptimeString);// set current item qe_sTime innerHTML to ptimeString
|
||||
|
||||
if($(".pluid-" + startTimes[0][i]) != null && $(".pluid-" + startTimes[0][i]).data("media") != null){
|
||||
st.setTime(st.getTime() + ($(".pluid-" + startTimes[0][i]).data("media").seconds * 1000));//calc end time
|
||||
}
|
||||
|
||||
$(".pluid-" + startTimes[0][i]).children(".qe_etime").text("enddate: " + st.toLocaleTimeString().replace(" ","") + " " + st.toLocaleDateString());//Set endtime (lil dirty but so is this script :P)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,18 +65,21 @@ function calcRefs(){
|
|||
function expandItem(itm){
|
||||
itm.find(".btn-group").show("blind");
|
||||
itm.find(".qe_time").show("blind");
|
||||
itm.find(".qe_etime").show("blind");
|
||||
itm.find(".qe_pref").show("blind");
|
||||
}
|
||||
|
||||
function collapseItem(itm){
|
||||
itm.find(".btn-group").hide("blind");
|
||||
itm.find(".qe_time").hide("blind");
|
||||
itm.find(".qe_etime").hide("blind");
|
||||
itm.find(".qe_pref").hide("blind");
|
||||
}
|
||||
|
||||
function toggleItem(itm){
|
||||
itm.find(".btn-group").toggle("blind");
|
||||
itm.find(".qe_time").toggle("blind");
|
||||
itm.find(".qe_etime").toggle("blind");
|
||||
itm.find(".qe_pref").toggle("blind");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,17 @@
|
|||
|
||||
Player.prototype.unlatch = function() {
|
||||
if (!this.seeklatch) {
|
||||
if (this.latched) {
|
||||
|
||||
var _this = this;
|
||||
this.getTime(function(ct){//checks to make sure the video isn't starting as this gets called like crazy :P
|
||||
if(ct > 0){
|
||||
if (_this.latched) {
|
||||
$("#latchvid").show();
|
||||
}
|
||||
return this.latched = false;
|
||||
_this.latched = false;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return this.seeklatch = false;
|
||||
}
|
||||
|
|
@ -242,7 +249,8 @@
|
|||
}
|
||||
this.setMediaProperties(data);
|
||||
this.pauseSeekRaceCondition = false;
|
||||
waitUntilDefined(window, 'YT', (function(_this) {
|
||||
if(USEROPTS.yt_source == "OFYT"){
|
||||
waitUntilDefined(window, 'YT', (function(_this) {//THIS IS WHERE YT VIDEO EMBED IS CREATED, RIP DIS BITCH!
|
||||
return function() {
|
||||
return waitUntilDefined(YT, 'Player', function() {
|
||||
var wmode;
|
||||
|
|
@ -266,9 +274,22 @@
|
|||
});
|
||||
};
|
||||
})(this));
|
||||
}else{
|
||||
var video;
|
||||
video = $('<iframe/>');
|
||||
removeOld(video);
|
||||
video.attr({
|
||||
src: "https://" + USEROPTS.yt_source + "/embed/" + data.id,
|
||||
webkitallowfullscreen: true,
|
||||
mozallowfullscreen: true,
|
||||
allowfullscreen: true
|
||||
});
|
||||
this.inv = video;
|
||||
}
|
||||
}
|
||||
|
||||
YouTubePlayer.prototype.load = function(data) {
|
||||
|
||||
this.setMediaProperties(data);
|
||||
if (this.yt && this.yt.ready) {
|
||||
return this.yt.loadVideoById(data.id, data.currentTime);
|
||||
|
|
@ -279,19 +300,29 @@
|
|||
|
||||
YouTubePlayer.prototype.onReady = function() {
|
||||
this.yt.ready = true;
|
||||
this.latched = true;
|
||||
handleVideoResize();
|
||||
return this.setVolume(VOLUME);
|
||||
};
|
||||
|
||||
YouTubePlayer.prototype.onStateChange = function(ev) {
|
||||
setMini();
|
||||
if (!CLIENT.leader && ev.data >= 2) {
|
||||
this.unlatch();
|
||||
}
|
||||
|
||||
if (ev.data === YT.PlayerState.PLAYING && this.pauseSeekRaceCondition) {
|
||||
this.pause();
|
||||
this.pauseSeekRaceCondition = false;
|
||||
|
||||
|
||||
}
|
||||
if ((ev.data === YT.PlayerState.PAUSED && !this.paused) || (ev.data === YT.PlayerState.PLAYING && this.paused)) {
|
||||
this.paused = ev.data === YT.PlayerState.PAUSED;
|
||||
if (CLIENT.leader) {
|
||||
sendVideoUpdate();
|
||||
}
|
||||
|
||||
}
|
||||
if (ev.data === YT.PlayerState.ENDED && CLIENT.leader) {
|
||||
return socket.emit('playNext');
|
||||
|
|
@ -319,6 +350,7 @@
|
|||
};
|
||||
|
||||
YouTubePlayer.prototype.setVolume = function(volume) {
|
||||
setMini();
|
||||
if (this.yt && this.yt.ready) {
|
||||
if (volume > 0) {
|
||||
this.yt.unMute();
|
||||
|
|
@ -329,6 +361,10 @@
|
|||
|
||||
YouTubePlayer.prototype.setQuality = function(quality) {};
|
||||
|
||||
YouTubePlayer.prototype.getRes = function(cb) {
|
||||
return cb([1920,1080]);
|
||||
};
|
||||
|
||||
YouTubePlayer.prototype.getTime = function(cb) {
|
||||
if (this.yt && this.yt.ready) {
|
||||
return cb(this.yt.getCurrentTime());
|
||||
|
|
@ -618,6 +654,8 @@
|
|||
}
|
||||
|
||||
VideoJSPlayer.prototype.loadPlayer = function(data) {
|
||||
this.latched = true;
|
||||
this.ofyt = data.ofyt;
|
||||
return waitUntilDefined(window, 'videojs', (function(_this) {
|
||||
return function() {
|
||||
var attrs, video;
|
||||
|
|
@ -716,7 +754,6 @@
|
|||
});
|
||||
_this.player.on('canplay', function() {
|
||||
handleWindowResize();
|
||||
return console.log(_this.player.children);
|
||||
});
|
||||
_this.player.on('timeupdate', function() {
|
||||
return setDur();
|
||||
|
|
@ -1729,13 +1766,24 @@
|
|||
error = error1;
|
||||
console.error(error);
|
||||
}
|
||||
if (data.meta.direct && data.type === 'vi') {
|
||||
if ((data.meta.direct && data.type === 'vi')) {
|
||||
try {
|
||||
return window.PLAYER = new VideoJSPlayer(data);
|
||||
} catch (error1) {
|
||||
e = error1;
|
||||
return console.error(e);
|
||||
}
|
||||
} else if ((USEROPTS.yt_source !== "OFYT" && data.type == "yt") || data.type == "dm") {
|
||||
data.ofyt = data.id;
|
||||
data.id = data.meta.rawLink;//set link and spoof mov/h264
|
||||
data.type = "fi";
|
||||
data.meta.codec = "mov/h264";
|
||||
try {
|
||||
return window.PLAYER = new FilePlayer(data);
|
||||
} catch (error1) {
|
||||
e = error1;
|
||||
return console.error(e);
|
||||
}
|
||||
}else if (data.type in TYPE_MAP) {
|
||||
try {
|
||||
return window.PLAYER = TYPE_MAP[data.type](data);
|
||||
|
|
@ -1751,6 +1799,7 @@
|
|||
PLAYER = window.PLAYER;
|
||||
dispSTimes();
|
||||
PLAYER.lastSTime = data.currentTime;
|
||||
setDur();
|
||||
if (!PLAYER.latched) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1763,6 +1812,7 @@
|
|||
data.currentTime = 0;
|
||||
}
|
||||
PLAYER.load(data);
|
||||
PLAYER.latchseek();
|
||||
PLAYER.play();
|
||||
}
|
||||
if (waiting) {
|
||||
|
|
|
|||
208
www/js/ui.js
208
www/js/ui.js
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*;
|
||||
fore.st is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
|
|
@ -103,7 +103,7 @@ function moveSplit(dper, mx){//Gross and hacky, but this prevents code re-use
|
|||
}
|
||||
}
|
||||
|
||||
$("#fpaneldiv").outerWidth($("#chatwrap").outerWidth() * 0.7);
|
||||
sizeFPDiv();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -120,8 +120,10 @@ $("#main").mouseup(function(evnt){
|
|||
$("#main").css("user-select", "auto");
|
||||
});
|
||||
|
||||
$("#cinemode").click(function(){
|
||||
if($("#footer").is(":visible") || $(".navbar").is(":visible") || $("#motdwrap").is(":visible")){
|
||||
$("#cinemode").click(cinemaMode);
|
||||
function cinemaMode(srun){
|
||||
var cm;
|
||||
if(cm = ($("#footer").is(":visible") || $(".navbar").is(":visible") || $("#motdwrap").is(":visible"))){
|
||||
motdOpen = $("#motdwrap").is(":visible");
|
||||
$("#motdwrap").hide( "blind");
|
||||
$("#footer").hide("blind");
|
||||
|
|
@ -133,7 +135,12 @@ $("#cinemode").click(function(){
|
|||
$("#footer").show("blind");
|
||||
$(".navbar").show("blind", function(){handleWindowResize()});
|
||||
}
|
||||
});
|
||||
|
||||
if(srun){
|
||||
USEROPTS.cinema_mode = cm;
|
||||
storeOpts();
|
||||
}
|
||||
}
|
||||
|
||||
$("#lockaspect").click(function() {
|
||||
RATIO_LOCKED = true;
|
||||
|
|
@ -144,36 +151,47 @@ $("#lockaspect").click(function() {
|
|||
/* chatbox */
|
||||
$("#blindchat").click(function() {
|
||||
if($("#chatwrap").is(":visible")){
|
||||
$("#blindchat").css("rotate","270deg");
|
||||
$("#blindchat").removeClass("glyphicon-chevron-down");
|
||||
$("#blindchat").addClass("glyphicon-chevron-right");
|
||||
}else{
|
||||
$("#blindchat").css("rotate","");
|
||||
$("#blindchat").removeClass("glyphicon-chevron-right");
|
||||
$("#blindchat").addClass("glyphicon-chevron-down");
|
||||
}
|
||||
blindChat();
|
||||
});
|
||||
|
||||
$("#modflair").click(function () {
|
||||
$("#modflair").click(modflair);
|
||||
function modflair(){
|
||||
var m = $("#modflair");
|
||||
var q = $("#qt-modflair");
|
||||
if (m.hasClass("label-success")) {
|
||||
USEROPTS.modhat = false;
|
||||
m.removeClass("label-success");
|
||||
q.removeClass("label-success");
|
||||
if (SUPERADMIN) {
|
||||
USEROPTS.adminhat = true;
|
||||
m.addClass("label-admin");
|
||||
q.addClass("label-admin");
|
||||
} else {
|
||||
m.addClass("label-default");
|
||||
q.addClass("label-default");
|
||||
}
|
||||
} else if (m.hasClass("label-admin")) {
|
||||
USEROPTS.adminhat = false;
|
||||
m.removeClass("label-admin")
|
||||
.addClass("label-default");
|
||||
q.removeClass("label-admin")
|
||||
.addClass("label-default");
|
||||
} else {
|
||||
USEROPTS.modhat = true;
|
||||
m.removeClass("label-default")
|
||||
.addClass("label-success");
|
||||
q.removeClass("label-default")
|
||||
.addClass("label-success");
|
||||
}
|
||||
$("#us-modflair").prop("checked", USEROPTS.modhat);
|
||||
setOpt('modhat', USEROPTS.modhat);
|
||||
});
|
||||
}
|
||||
|
||||
$("#usercount").mouseenter(function (ev) {
|
||||
var breakdown = calcUserBreakdown();
|
||||
|
|
@ -198,8 +216,8 @@ $("#usercount").mousemove(function (ev) {
|
|||
if(popup.length == 0)
|
||||
return;
|
||||
|
||||
popup.css("top", (ev.clientY + 5) + "px");
|
||||
popup.css("left", (ev.clientX - 120) + "px");
|
||||
popup.css("top", (ev.clientY + 5) - $("#usercount").parent().offset().top + "px");
|
||||
popup.css("left", (ev.clientX - 120) - $("#usercount").parent().offset().left + "px");
|
||||
});
|
||||
|
||||
$("#usercount").mouseleave(function () {
|
||||
|
|
@ -243,13 +261,32 @@ CyTube.chatTabCompleteData = {
|
|||
context: {}
|
||||
};
|
||||
|
||||
function chatTabComplete(chatline) {
|
||||
function chatTabComplete(chatline, hint) {
|
||||
var servercmd = [
|
||||
["me", -1],
|
||||
["announce", 2],
|
||||
["af", 256],
|
||||
["sp", -1],
|
||||
["afk", -1],
|
||||
["poll", "pollctl"],
|
||||
["hpoll", "pollctl"],
|
||||
["mute", "mute"],
|
||||
["smute", "mute"],
|
||||
["unmite", "mute"],
|
||||
["kick", "kick"],
|
||||
["ban", "ban"],
|
||||
["ipban", "ban"],
|
||||
["clear", 2],
|
||||
["resettoke", 2],
|
||||
["cleantitle", 2],
|
||||
["sendcmd", 256],
|
||||
];//serverside commands [cmd,reqrank/reqperm]
|
||||
if (!CyTube.tabCompleteMethods) {
|
||||
console.error('Missing CyTube.tabCompleteMethods!');
|
||||
return;
|
||||
}
|
||||
var currentText = chatline.value;
|
||||
var currentPosition = chatline.selectionEnd;
|
||||
var currentPosition = hint ? chatline.value.length : chatline.selectionEnd;
|
||||
if (typeof currentPosition !== 'number' || !chatline.setSelectionRange) {
|
||||
// Bail, we're on IE8 or something similarly dysfunctional
|
||||
return;
|
||||
|
|
@ -268,13 +305,26 @@ function chatTabComplete(chatline) {
|
|||
let tokeList = loadTokes();
|
||||
if(tokeList === undefined || tokeList === null){
|
||||
tokeList = [];//create var
|
||||
tokeList.push("!toke");
|
||||
tokeList.push("!toke");//manually push the two defaults
|
||||
tokeList.push("!r");
|
||||
}
|
||||
|
||||
for (var i = 0; i < tokeList.length; i++) {
|
||||
options.push(tokeList[i]);
|
||||
}
|
||||
|
||||
servercmd.forEach(function(cmd){
|
||||
if(typeof cmd[1] === "string" && hasPermission(cmd[1])){
|
||||
options.push("!" + cmd[0]);
|
||||
}else if(CLIENT.rank >= cmd[1]){
|
||||
options.push("!" + cmd[0]);
|
||||
}
|
||||
});
|
||||
|
||||
commands.forEach(function(cfunc, cmd){
|
||||
options.push("/" + cmd);
|
||||
});
|
||||
|
||||
CHANNEL.emotes.forEach(function (emote) {
|
||||
options.push(emote.name);
|
||||
});
|
||||
|
|
@ -292,19 +342,52 @@ function chatTabComplete(chatline) {
|
|||
CyTube.chatTabCompleteData.context
|
||||
);
|
||||
|
||||
if(hint){
|
||||
chathint(result.text);
|
||||
}else{
|
||||
chathint("");
|
||||
chatline.value = result.text;
|
||||
chatline.setSelectionRange(result.newPosition, result.newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
function callChat(){
|
||||
if (CHATTHROTTLE) {
|
||||
return;
|
||||
}
|
||||
var msg = $("#chatline").val();
|
||||
|
||||
var qcmd = msg.indexOf("/") === 0;
|
||||
var rcmd = qcmd ? msg : msg.replace(/\s+/,"");
|
||||
|
||||
//if (data.msg.indexOf("/") === 0) {Legacy from cytube '/' commands
|
||||
if (rcmd.indexOf("/") === 0){
|
||||
var space = rcmd.indexOf(" ");
|
||||
var cmd;
|
||||
if (space < 0) {
|
||||
cmd = rcmd.substring(1);
|
||||
} else {
|
||||
cmd = rcmd.substring(1, space);
|
||||
}
|
||||
|
||||
cfunc = commands.get(cmd);
|
||||
|
||||
if(typeof cfunc === "function"){
|
||||
cfunc(msg);
|
||||
if (msg.indexOf("/") === 0) {
|
||||
CHATHIST.push($("#chatline").val());
|
||||
CHATHISTIDX = CHATHIST.length;
|
||||
$("#chatline").val("");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(msg.trim()) {
|
||||
var meta = {};
|
||||
|
||||
if (USEROPTS.adminhat && CLIENT.rank >= 255) {
|
||||
msg = "!a " + msg;
|
||||
msg = "!af " + msg;
|
||||
} else if (USEROPTS.modhat && CLIENT.rank >= Rank.Moderator) {
|
||||
meta.modflair = CLIENT.rank;
|
||||
}
|
||||
|
|
@ -315,10 +398,6 @@ function callChat(){
|
|||
msg = msg.substring(3);
|
||||
}
|
||||
|
||||
if (msg.charAt(0) === '!' && msg != "!toke"){
|
||||
storeToke(msg);
|
||||
}
|
||||
|
||||
socket.emit("chatMsg", {
|
||||
msg: msg,
|
||||
meta: meta
|
||||
|
|
@ -365,6 +444,7 @@ $("#chatline").keydown(function(ev) {
|
|||
ev.preventDefault();
|
||||
return false;
|
||||
}
|
||||
setTimeout(function(){chatTabComplete(ev.target, true);},1);//set timeout to give browser time to fill textbox, still more responsive than waiting for keyup
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -460,9 +540,11 @@ $("#latchvid").click(function() {
|
|||
|
||||
$("#blindvideo").click(function() {
|
||||
if($("#videowrap").is(":visible")){
|
||||
$("#blindvideo").css("rotate","270deg");
|
||||
$("#blindvideo").removeClass("glyphicon-chevron-down");
|
||||
$("#blindvideo").addClass("glyphicon-chevron-right");
|
||||
}else{
|
||||
$("#blindvideo").css("rotate","");
|
||||
$("#blindvideo").removeClass("glyphicon-chevron-right");
|
||||
$("#blindvideo").addClass("glyphicon-chevron-down");
|
||||
}
|
||||
blindVideo();
|
||||
});
|
||||
|
|
@ -527,6 +609,8 @@ function setDur(){
|
|||
/* playlist controls */
|
||||
|
||||
$("#queue").sortable({
|
||||
scroll: true,
|
||||
containment: "parent",
|
||||
start: function(ev, ui) {
|
||||
PL_FROM = ui.item.data("uid");
|
||||
},
|
||||
|
|
@ -602,6 +686,7 @@ function queue(pos, src) {
|
|||
var duration = undefined;
|
||||
var title = undefined;
|
||||
var subtitle = "";
|
||||
var minDuration = 0;
|
||||
if (data.type === "fi") {
|
||||
if (data.id.match(/^http:/)) {
|
||||
Callbacks.queueFail({
|
||||
|
|
@ -632,7 +717,12 @@ function queue(pos, src) {
|
|||
// Raw files allow title overrides since the ffprobe tag data
|
||||
// is not always correct.
|
||||
title = $("#addfromurl-title-val").val();
|
||||
subtitle = $("#addfromurl-subtitle-val").val();
|
||||
subtitle = "";
|
||||
}else if(data.type === "ia"){
|
||||
title = $("#addfromurl-title-val").val();
|
||||
minDuration = $("#addfromurl-duration-val").val();
|
||||
//Convert minDuration to a number that represents seconds instead of a string which represents minutes
|
||||
minDuration = (minDuration == "" ? 0 : (Number.parseInt(minDuration) * 60));
|
||||
}
|
||||
|
||||
if (data.id == null || data.type == null) {
|
||||
|
|
@ -647,6 +737,7 @@ function queue(pos, src) {
|
|||
pos: pos,
|
||||
duration: duration,
|
||||
title: title,
|
||||
minDuration: minDuration,
|
||||
temp: addTemp,
|
||||
link: link,
|
||||
subtitle: subtitle
|
||||
|
|
@ -686,15 +777,28 @@ $("#queue_end").click(queue.bind(this, "end", "url"));
|
|||
$("#ce_queue_next").click(queue.bind(this, "next", "customembed"));
|
||||
$("#ce_queue_end").click(queue.bind(this, "end", "customembed"));
|
||||
|
||||
$("#golive").click(function(ev){
|
||||
socket.emit('queue', {
|
||||
id: "https://stream.ourfore.st",
|
||||
type: 'hl',
|
||||
pos: "next",
|
||||
minDuration: 0,
|
||||
temp: true,
|
||||
subtitle: ''
|
||||
});
|
||||
});
|
||||
|
||||
$("#mediaurl").keyup(function(ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
queue("end", "url");
|
||||
} else {
|
||||
var editTitle = false;
|
||||
var editDur = false;
|
||||
try {
|
||||
if (parseMediaLink($("#mediaurl").val()).type === "fi") {
|
||||
editTitle = true;
|
||||
}
|
||||
editTitle = (parseMediaLink($("#mediaurl").val()).type === "fi" || parseMediaLink($("#mediaurl").val()).type === "ia");
|
||||
|
||||
|
||||
editDur = (parseMediaLink($("#mediaurl").val()).type === "ia");
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
|
|
@ -720,10 +824,11 @@ $("#mediaurl").keyup(function(ev) {
|
|||
})
|
||||
.appendTo($("#addfromurl-title")).show("blind");//append and show
|
||||
|
||||
if(editDur){
|
||||
$("<input/>").addClass("form-control")//create title field
|
||||
.attr("type", "text")//the attributes
|
||||
.attr("id", "addfromurl-subtitle-val")
|
||||
.attr("placeholder", "Alternate Subtitle Track")
|
||||
.attr("id", "addfromurl-duration-val")
|
||||
.attr("placeholder", "Minimum Duration Filter")
|
||||
.attr("style", "display: none; width: 100%;")
|
||||
.keydown(function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
|
|
@ -732,6 +837,8 @@ $("#mediaurl").keyup(function(ev) {
|
|||
})
|
||||
.appendTo($("#addfromurl-title")).show("blind");//append and show
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
$("#addfromurl-title").hide("blind");
|
||||
$("#addfromurl-title").remove();//otherwise remove
|
||||
|
|
@ -757,10 +864,15 @@ $("#voteskip").click(function() {
|
|||
$("#blindItems").click(function(){
|
||||
$("#blindItems").toggleClass("glyphicon-resize-small");
|
||||
$("#blindItems").toggleClass("glyphicon-resize-full");
|
||||
$("#blindItems").prop("title",
|
||||
($("#blindItems").prop("title") == "Collapse All Items") ? "Expand All Items" : "Collapse All Items"
|
||||
)
|
||||
toggleItems();
|
||||
|
||||
if($("#blindItems").prop("title") == "Collapse All Items"){
|
||||
$("#blindItems").prop("title","Expand All Items");
|
||||
collapseItems();
|
||||
}else{
|
||||
$("#blindItems").prop("title","Collapse All Items");
|
||||
expandItems();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$("#hideplaylist").click(function(){
|
||||
|
|
@ -785,34 +897,7 @@ $("#getplaylist").click(function() {
|
|||
if (idx >= 0) {
|
||||
socket.listeners("playlist").splice(idx);
|
||||
}
|
||||
var list = [];
|
||||
for(var i = 0; i < data.length; i++) {
|
||||
var entry = formatURL(data[i].media);
|
||||
list.push(entry);
|
||||
}
|
||||
var urls = list.join(",");
|
||||
|
||||
var outer = $("<div/>").addClass("modal fade")
|
||||
.appendTo($("body"));
|
||||
modal = $("<div/>").addClass("modal-dialog").appendTo(outer);
|
||||
modal = $("<div/>").addClass("modal-content").appendTo(modal);
|
||||
var head = $("<div/>").addClass("modal-header")
|
||||
.appendTo(modal);
|
||||
$("<button/>").addClass("close")
|
||||
.attr("data-dismiss", "modal")
|
||||
.attr("aria-hidden", "true")
|
||||
.html("×")
|
||||
.appendTo(head);
|
||||
$("<h3/>").text("Playlist URLs").appendTo(head);
|
||||
var body = $("<div/>").addClass("modal-body").appendTo(modal);
|
||||
$("<input/>").addClass("form-control").attr("type", "text")
|
||||
.val(urls)
|
||||
.appendTo(body);
|
||||
$("<div/>").addClass("modal-footer").appendTo(modal);
|
||||
outer.on("hidden.bs.modal", function() {
|
||||
outer.remove();
|
||||
});
|
||||
outer.modal();
|
||||
fpexport.popMenu(data);
|
||||
};
|
||||
socket.on("playlist", callback);
|
||||
var errCallback = function(data) {
|
||||
|
|
@ -1077,13 +1162,16 @@ $("#cs-emotes-import").click(function () {
|
|||
|
||||
var toggleUserlist = function () {
|
||||
var direction = "glyphicon-chevron-left"
|
||||
if ($("#userlist")[0].style.display === "none") {
|
||||
var exp;
|
||||
if (exp = ($("#userlist")[0].style.display === "none")) {
|
||||
$("#userlist").show();
|
||||
$("#userlisttoggle").removeClass(direction).addClass("glyphicon-chevron-down");
|
||||
} else {
|
||||
$("#userlist").hide();
|
||||
$("#userlisttoggle").removeClass("glyphicon-chevron-down").addClass(direction);
|
||||
}
|
||||
USEROPTS.show_userlist = exp;
|
||||
storeOpts();
|
||||
scrollChat();
|
||||
};
|
||||
|
||||
|
|
|
|||
206
www/js/util.js
206
www/js/util.js
|
|
@ -184,6 +184,9 @@ function formatUserlistItem(div) {
|
|||
$("<br/>").appendTo(profile);
|
||||
$("<em/>").text(meta.ip).appendTo(profile);
|
||||
}
|
||||
|
||||
$("<br/>").appendTo(profile);
|
||||
$("<em/>").text("tokes: " + div.data().meta.toke).appendTo(profile);
|
||||
if (meta.aliases) {
|
||||
$("<br/>").appendTo(profile);
|
||||
$("<em/>").text("aliases: " + meta.aliases.join(", ")).appendTo(profile);
|
||||
|
|
@ -195,10 +198,11 @@ function formatUserlistItem(div) {
|
|||
profile.css("left", horiz + "px")
|
||||
});
|
||||
name.mousemove(function(ev) {
|
||||
var top = ev.clientY + 5;
|
||||
var horiz = ev.clientX;
|
||||
|
||||
if ($("body").hasClass("synchtube")) horiz -= profile.outerWidth();
|
||||
var top = ev.clientY + 5 - div.parent().offset().top;
|
||||
var horiz = ev.clientX - div.offset().left;
|
||||
|
||||
//if ($("body").hasClass("synchtube")) horiz -= profile.outerWidth();
|
||||
profile.css("left", horiz + "px")
|
||||
.css("top", top + "px");
|
||||
});
|
||||
|
|
@ -244,13 +248,15 @@ function addUserDropdown(entry) {
|
|||
.appendTo(entry)
|
||||
.hide();
|
||||
|
||||
var istokebot = (name === "tokebot");
|
||||
|
||||
$("<strong/>").text(name).appendTo(menu);
|
||||
$("<br/>").appendTo(menu);
|
||||
|
||||
var btngroup = $("<div/>").addClass("btn-group-vertical").appendTo(menu);
|
||||
|
||||
/* give/remove leader (moderator+ only) */
|
||||
if (hasPermission("leaderctl")) {
|
||||
if (hasPermission("leaderctl") && !istokebot) {
|
||||
var ldr = $("<button/>").addClass("btn btn-xs btn-default")
|
||||
.appendTo(btngroup);
|
||||
if(leader) {
|
||||
|
|
@ -271,7 +277,7 @@ function addUserDropdown(entry) {
|
|||
}
|
||||
|
||||
/* pm button */
|
||||
if (name !== CLIENT.name) {
|
||||
if (name !== CLIENT.name && name !== "tokebot") {
|
||||
var pm = $("<button/>").addClass("btn btn-xs btn-default")
|
||||
.text("Private Message")
|
||||
.appendTo(btngroup)
|
||||
|
|
@ -304,7 +310,7 @@ function addUserDropdown(entry) {
|
|||
});
|
||||
|
||||
/* ignore button */
|
||||
if (name !== CLIENT.name) {
|
||||
if (name !== CLIENT.name && !istokebot) {
|
||||
var ignore = $("<button/>").addClass("btn btn-xs btn-default")
|
||||
.appendTo(btngroup)
|
||||
.click(function () {
|
||||
|
|
@ -329,7 +335,7 @@ function addUserDropdown(entry) {
|
|||
}
|
||||
|
||||
/* mute buttons */
|
||||
if (hasPermission("mute")) {
|
||||
if (hasPermission("mute") && !istokebot) {
|
||||
var mute = $("<button/>").addClass("btn btn-xs btn-default")
|
||||
.text("Mute")
|
||||
.click(function () {
|
||||
|
|
@ -366,7 +372,7 @@ function addUserDropdown(entry) {
|
|||
}
|
||||
|
||||
/* kick button */
|
||||
if(hasPermission("kick")) {
|
||||
if(hasPermission("kick") && !istokebot) {
|
||||
$("<button/>").addClass("btn btn-xs btn-default")
|
||||
.text("Kick")
|
||||
.click(function () {
|
||||
|
|
@ -383,7 +389,7 @@ function addUserDropdown(entry) {
|
|||
}
|
||||
|
||||
/* ban buttons */
|
||||
if(hasPermission("ban")) {
|
||||
if(hasPermission("ban") && !istokebot) {
|
||||
$("<button/>").addClass("btn btn-xs btn-default")
|
||||
.text("Name Ban")
|
||||
.click(function () {
|
||||
|
|
@ -477,6 +483,31 @@ function calcUserBreakdown() {
|
|||
function sortUserlist() {
|
||||
var slice = Array.prototype.slice;
|
||||
var list = slice.call($("#userlist .userlist_item"));
|
||||
if(//check if tokebot listing is present
|
||||
list.filter(function(u,i){
|
||||
return($(u).children()[1].innerHTML === "tokebot");
|
||||
}).length <= 0
|
||||
){//inject tokebot userlist entry
|
||||
CyTube._internal_do_not_use_or_you_will_be_banned.addUserToList(
|
||||
{
|
||||
name: "tokebot",
|
||||
rank: 3,
|
||||
profile: {
|
||||
test: "!TOKE OR DIE!",
|
||||
image: window.location.origin + "/img/femotes/tokebot.jpg"
|
||||
},
|
||||
meta: {
|
||||
afk: false,
|
||||
aliases: ["tokebot"],
|
||||
ip: "127.0.0.1",
|
||||
muted: false,
|
||||
smuted: false,
|
||||
toke: '∞'
|
||||
}
|
||||
}
|
||||
);
|
||||
var list = slice.call($("#userlist .userlist_item"));//pull list again, make sure tokebot gets sorted
|
||||
}
|
||||
list.sort(function (a, b) {
|
||||
var r1 = $(a).data("rank");
|
||||
var r2 = $(b).data("rank");
|
||||
|
|
@ -512,15 +543,15 @@ function sortUserlist() {
|
|||
|
||||
/* queue stuff */
|
||||
|
||||
function scrollQueue() {
|
||||
function scrollQueue(ovr) {
|
||||
var li = playlistFind(PL_CURRENT);
|
||||
if(!li)
|
||||
if(!li || (!USEROPTS.scroll_list && !ovr))
|
||||
return;
|
||||
|
||||
li = $(li);
|
||||
$("#queue").scrollTop(0);
|
||||
var scroll = li.position().top - $("#queue").position().top;
|
||||
$("#queue").scrollTop(scroll);
|
||||
$("#queue").scrollTop(scroll - (!USEROPTS.show_playlist ? $("#rightcontrols").outerHeight() + 1 : 0));
|
||||
}
|
||||
|
||||
function makeQueueEntry(item, addbtns) {
|
||||
|
|
@ -543,8 +574,7 @@ function makeQueueEntry(item, addbtns) {
|
|||
.attr("target", "_blank");
|
||||
var sTime = $("<span/>").addClass("qe_sTime").appendTo(li);
|
||||
$("<br/>").appendTo(li);
|
||||
var pref = $("<span/>").addClass("qe_pref").appendTo(li);
|
||||
pref.text(" \n");
|
||||
var etime = $("<span/>").addClass("qe_etime").appendTo(li);
|
||||
var time = $("<span/>").addClass("qe_time").appendTo(li);
|
||||
time.text("airtime: " + video.duration);
|
||||
//dispSTimes();
|
||||
|
|
@ -555,6 +585,8 @@ function makeQueueEntry(item, addbtns) {
|
|||
|
||||
if(addbtns)
|
||||
addQueueButtons(li);
|
||||
|
||||
//var pref = $("<span/>").addClass("qe_pref").appendTo(li);
|
||||
return li;
|
||||
}
|
||||
|
||||
|
|
@ -705,6 +737,7 @@ function showUserOptions() {
|
|||
$("#us-theme").val(USEROPTS.theme);
|
||||
$("#us-no-channelcss").prop("checked", USEROPTS.ignore_channelcss);
|
||||
$("#us-no-channeljs").prop("checked", USEROPTS.ignore_channeljs);
|
||||
$("#us-chat-min").val(USEROPTS.chat_min);
|
||||
|
||||
$("#us-synch").prop("checked", USEROPTS.synch);
|
||||
$("#us-synch-accuracy").val(USEROPTS.sync_accuracy);
|
||||
|
|
@ -713,6 +746,7 @@ function showUserOptions() {
|
|||
$("#us-playlistbuttons").prop("checked", USEROPTS.qbtn_hide);
|
||||
$("#us-oldbtns").prop("checked", USEROPTS.qbtn_idontlikechange);
|
||||
$("#us-video-orientation").prop("checked", USEROPTS.show_orientation);
|
||||
$("#us-yt-source").val(USEROPTS.yt_source);
|
||||
$("#us-default-quality").val(USEROPTS.default_quality || "auto");
|
||||
|
||||
$("#us-chat-timestamp").prop("checked", USEROPTS.show_timestamps);
|
||||
|
|
@ -720,6 +754,8 @@ function showUserOptions() {
|
|||
$("#us-sort-rank").prop("checked", USEROPTS.sort_rank);
|
||||
$("#us-sort-afk").prop("checked", USEROPTS.sort_afk);
|
||||
$("#us-legacy-emote").prop("checked", USEROPTS.legacy_emote);
|
||||
$("#us-toke-pm").prop("checked", USEROPTS.toke_pm);
|
||||
$("#us-whisper-clear").val(USEROPTS.whisper_clear);
|
||||
$("#us-blink-title").val(USEROPTS.blink_title);
|
||||
$("#us-ping-sound").val(USEROPTS.boop);
|
||||
$("#us-notifications").val(USEROPTS.notifications);
|
||||
|
|
@ -730,6 +766,11 @@ function showUserOptions() {
|
|||
$("#us-modflair").prop("checked", USEROPTS.modhat);
|
||||
$("#us-shadowchat").prop("checked", USEROPTS.show_shadowchat);
|
||||
$("#us-show-ip-in-tooltip").prop("checked", USEROPTS.show_ip_in_tooltip);
|
||||
$("#us-show-playlist").prop("checked", USEROPTS.show_playlist);
|
||||
$("#us-scroll-playlist").prop("checked", USEROPTS.scroll_list);
|
||||
$("#us-add-invid").prop("checked", USEROPTS.add_invid);
|
||||
|
||||
$("#us-no-lightning").prop("checked", USEROPTS.no_lightning);
|
||||
|
||||
formatScriptAccessPrefs();
|
||||
|
||||
|
|
@ -742,7 +783,12 @@ function saveUserOptions() {
|
|||
createCookie("cytube-theme", USEROPTS.theme, 1000);
|
||||
USEROPTS.ignore_channelcss = $("#us-no-channelcss").prop("checked");
|
||||
USEROPTS.ignore_channeljs = $("#us-no-channeljs").prop("checked");
|
||||
USEROPTS.chat_min = ($("#us-chat-min").val() >= 80 ? 80 : $("#us-chat-min").val());
|
||||
|
||||
USEROPTS.show_ip_in_tooltip = $("#us-show-ip-in-tooltip").prop("checked");
|
||||
USEROPTS.show_playlist = $("#us-show-playlist").prop("checked");
|
||||
USEROPTS.scroll_list = $("#us-scroll-playlist").prop("checked");
|
||||
USEROPTS.add_invid = $("#us-add-invid").prop("checked");
|
||||
|
||||
USEROPTS.synch = $("#us-synch").prop("checked");
|
||||
USEROPTS.sync_accuracy = parseFloat($("#us-synch-accuracy").val()) || 2;
|
||||
|
|
@ -751,6 +797,7 @@ function saveUserOptions() {
|
|||
USEROPTS.qbtn_hide = $("#us-playlistbuttons").prop("checked");
|
||||
USEROPTS.qbtn_idontlikechange = $("#us-oldbtns").prop("checked");
|
||||
USEROPTS.show_orientation = $("#us-video-orientation").prop("checked");
|
||||
USEROPTS.yt_source = $("#us-yt-source").val();
|
||||
USEROPTS.default_quality = $("#us-default-quality").val();
|
||||
|
||||
USEROPTS.show_timestamps = $("#us-chat-timestamp").prop("checked");
|
||||
|
|
@ -758,6 +805,8 @@ function saveUserOptions() {
|
|||
USEROPTS.sort_rank = $("#us-sort-rank").prop("checked");
|
||||
USEROPTS.sort_afk = $("#us-sort-afk").prop("checked");
|
||||
USEROPTS.legacy_emote = $("#us-legacy-emote").prop("checked");
|
||||
USEROPTS.toke_pm = $("#us-toke-pm").prop("checked");
|
||||
USEROPTS.whisper_clear = $("#us-whisper-clear").val();
|
||||
USEROPTS.blink_title = $("#us-blink-title").val();
|
||||
USEROPTS.boop = $("#us-ping-sound").val();
|
||||
USEROPTS.notifications = $("#us-notifications").val();
|
||||
|
|
@ -765,6 +814,8 @@ function saveUserOptions() {
|
|||
USEROPTS.strip_image = $("#us-strip-image").prop("checked");
|
||||
USEROPTS.chat_tab_method = $("#us-chat-tab-method").val();
|
||||
|
||||
USEROPTS.no_lightning = $("#us-no-lightning").prop("checked");
|
||||
|
||||
if (CLIENT.rank >= 2) {
|
||||
USEROPTS.modhat = $("#us-modflair").prop("checked");
|
||||
USEROPTS.show_shadowchat = $("#us-shadowchat").prop("checked");
|
||||
|
|
@ -784,6 +835,13 @@ function storeOpts() {
|
|||
}
|
||||
|
||||
function applyOpts() {
|
||||
handleVideoResize();
|
||||
|
||||
if(!USEROPTS.show_userlist){
|
||||
$("#userlist").hide();
|
||||
$("#userlisttoggle").removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-left");
|
||||
}
|
||||
|
||||
if ($("#usertheme").attr("href") !== USEROPTS.theme) {
|
||||
var old = $("#usertheme").attr("id", "usertheme_old");
|
||||
var theme = USEROPTS.theme;
|
||||
|
|
@ -813,6 +871,16 @@ function applyOpts() {
|
|||
removeVideo();
|
||||
}
|
||||
|
||||
if(hasPermission("playlistadd")){
|
||||
if(USEROPTS.show_playlist){
|
||||
$("#showplaylist").click();
|
||||
}else{
|
||||
$("#rightcontrols").hide();
|
||||
$("#playlistrow").hide();
|
||||
$("#showplaylist").hide();
|
||||
}
|
||||
}
|
||||
|
||||
$("#chatbtn").remove();
|
||||
if(false) {
|
||||
var btn = $("<button/>").addClass("btn btn-default btn-block")
|
||||
|
|
@ -850,6 +918,13 @@ function applyOpts() {
|
|||
USEROPTS.notifications = "never";
|
||||
}
|
||||
}
|
||||
if((USEROPTS.yt_source == "OFYT" && window.PLAYER.mediaType == "fi" && window.PLAYER.mediaId.includes("google")) || (USEROPTS.yt_source != "OFYT" && window.PLAYER.mediaType == "yt")){
|
||||
PLAYER.mediaType = "";
|
||||
PLAYER.mediaId = "";
|
||||
|
||||
console.log("switch");
|
||||
socket.emit("playerReady");
|
||||
}
|
||||
}
|
||||
|
||||
function parseTimeout(t) {
|
||||
|
|
@ -1075,6 +1150,7 @@ function handlePermissionChange() {
|
|||
setVisible("#showchansettings", CLIENT.rank >= 2);
|
||||
setVisible("#playlistmanagerwrap", CLIENT.rank >= 1);
|
||||
setVisible("#modflair", CLIENT.rank >= 2);
|
||||
setVisible("#modopenbtn", CLIENT.rank >= 2);
|
||||
setVisible("#guestlogin", CLIENT.rank < 0);
|
||||
setVisible("#chatline", CLIENT.rank >= 0);
|
||||
setVisible("#queue", hasPermission("seeplaylist"));
|
||||
|
|
@ -1118,6 +1194,8 @@ function handlePermissionChange() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(hasPermission("playlistmove")) {
|
||||
$("#queue").sortable("enable");
|
||||
$("#queue").addClass("queue_sortable");
|
||||
|
|
@ -1136,6 +1214,15 @@ function handlePermissionChange() {
|
|||
$(".add-temp").attr("disabled", false);
|
||||
}
|
||||
|
||||
if(CLIENT.rank > 2){
|
||||
$("#modopenbtn").html("ADMN").attr("title","Admin Panel");
|
||||
fpmod.title = "Admin";
|
||||
}else if(CLIENT.rank == 1 && (CHANNEL.name === "submit" || CHANNEL.name === "submitbump")){
|
||||
$("#modopenbtn")[0].onclick = function(){panelbtn(fpplaylist);};
|
||||
$("#modopenbtn").html("SUBMIT").attr("title","Submission List").show();
|
||||
$(".add-temp").attr("checked", false).attr("disabled",true);//enforce perminant submissions
|
||||
}
|
||||
|
||||
fixWeirdButtonAlignmentIssue();
|
||||
|
||||
setVisible("#newpollbtn", hasPermission("pollctl"));
|
||||
|
|
@ -1170,6 +1257,18 @@ function handlePermissionChange() {
|
|||
$("#chatline").attr("placeholder", "");
|
||||
}
|
||||
rebuildPlaylist();
|
||||
|
||||
if(!USEROPTS.show_playlist && $("#playlistrow").parent()[0].className == "container"){
|
||||
$("#rightcontrols").hide();
|
||||
$("#playlistrow").hide();
|
||||
$("#rightcontrols").hide();
|
||||
$("#playlistrow").hide();
|
||||
$("#showplaylist").hide();
|
||||
}
|
||||
|
||||
if(USEROPTS.cinema_mode){
|
||||
cinemaMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
function fixWeirdButtonAlignmentIssue() {
|
||||
|
|
@ -1516,6 +1615,13 @@ function parseMediaLink(url) {
|
|||
};
|
||||
}
|
||||
|
||||
if((m = url.match(/archive\.org\/(?:details|download)\/([a-zA-Z0-9_-]+)(?!.|\/)/))){
|
||||
return{
|
||||
id: m[1],
|
||||
type: "ia"
|
||||
}
|
||||
}
|
||||
|
||||
/* Shorthand URIs */
|
||||
// So we still trim DailyMotion URLs
|
||||
if((m = url.match(/^dm:([^\?&#_]+)/))) {
|
||||
|
|
@ -1539,6 +1645,7 @@ function parseMediaLink(url) {
|
|||
}
|
||||
// Generic for the rest.
|
||||
if ((m = url.match(/^([a-z]{2}):([^\?&#]+)/))) {
|
||||
|
||||
return {
|
||||
id: m[2],
|
||||
type: m[1]
|
||||
|
|
@ -1554,6 +1661,11 @@ function parseMediaLink(url) {
|
|||
id: url,
|
||||
type: "cm"
|
||||
};
|
||||
} else if(USEROPTS.add_invid && (m = url.match(/(?<=\/watch\?v\=).*/))) {//catch invidious links
|
||||
return {
|
||||
id: m[0],
|
||||
type: "yt"
|
||||
};
|
||||
} else {
|
||||
// Assume raw file (server will check)
|
||||
return {
|
||||
|
|
@ -1573,12 +1685,13 @@ function sendVideoUpdate() {
|
|||
if (!CLIENT.leader) {
|
||||
return;
|
||||
}
|
||||
console.log(PLAYER.ofyt ? "yt" : PLAYER.mediaType);
|
||||
PLAYER.getTime(function (seconds) {
|
||||
socket.emit("mediaUpdate", {
|
||||
id: PLAYER.mediaId,
|
||||
id: (PLAYER.ofyt != null ? PLAYER.ofyt : PLAYER.mediaId),
|
||||
currentTime: seconds,
|
||||
paused: PLAYER.paused,
|
||||
type: PLAYER.mediaType
|
||||
type: (PLAYER.ofyt != null ? "yt" : PLAYER.mediaType)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1688,6 +1801,7 @@ function formatChatMessage(data, last) {
|
|||
}
|
||||
|
||||
function addChatMessage(data) {
|
||||
|
||||
if(IGNORED.indexOf(data.username) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1696,6 +1810,15 @@ function addChatMessage(data) {
|
|||
}
|
||||
var msgBuf = $("#messagebuffer");
|
||||
var div = formatChatMessage(data, LASTCHAT);
|
||||
|
||||
if(data.meta.addClass === "server-whisper" && USEROPTS.whisper_clear > 0){
|
||||
setTimeout(function(){
|
||||
div.hide("blind",function(){
|
||||
div.remove();
|
||||
});
|
||||
},USEROPTS.whisper_clear * 1000);
|
||||
}
|
||||
|
||||
// Incoming: a bunch of crap for the feature where if you hover over
|
||||
// a message, it highlights messages from that user
|
||||
var safeUsername = data.username.replace(/[^\w-]/g, '\\$');
|
||||
|
|
@ -1821,6 +1944,7 @@ function handleWindowResize() {
|
|||
($("#motdwrap").is(":visible") ? $("#motdwrap").outerHeight() : 0) -
|
||||
($("#footer").is(":visible") ? $("#footer").outerHeight() : 0) -
|
||||
($(".navbar").is(":visible") ? $(".navbar").outerHeight() : 0) -
|
||||
($("#announcements").is(":visible") ? $("#announcements").outerHeight() : 0) -
|
||||
$("#chatheader").outerHeight() - 1;
|
||||
|
||||
var h = rawh - $("#chatline").outerHeight();
|
||||
|
|
@ -1869,8 +1993,8 @@ function handleWindowResize() {
|
|||
$("#videowrap").outerHeight(rheight);
|
||||
$(".embed-responsive").outerHeight(rheight);
|
||||
$("#ytapiplayer").outerHeight(rheight);
|
||||
h = h - rheight - $("#videowrap-header").outerHeight();
|
||||
rawh = rawh - rheight - $("#videowrap-header").outerHeight();
|
||||
h = h - ($("#videowrap").is(":visible") ? rheight : 0 )- $("#videowrap-header").outerHeight();
|
||||
rawh = rawh - ($("#videowrap").is(":visible") ? rheight : 0 ) - $("#videowrap-header").outerHeight();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1881,7 +2005,6 @@ function handleWindowResize() {
|
|||
$("#chatwrap").outerHeight(rawh);
|
||||
sizeFPDiv();//resize fpanel
|
||||
|
||||
|
||||
handleVideoResize();
|
||||
}
|
||||
|
||||
|
|
@ -1893,11 +2016,12 @@ function handleVideoResize() {//rewritten to adjust width to aspect ratio
|
|||
res = x != null ? x : [0,0];
|
||||
});
|
||||
|
||||
rwidth = (res[0]/res[1]) * parseInt($("#ytapiplayer").css("height"));
|
||||
var rwidth = (res[0]/res[1]) * parseInt($("#ytapiplayer").css("height"));
|
||||
var rper = ((rwidth/$("body").outerWidth()) * 100);
|
||||
var mper = (rper < (100 - USEROPTS.chat_min) ? rper:(100 - USEROPTS.chat_min))
|
||||
|
||||
rper = ((rwidth/$("body").outerWidth()) * 100);
|
||||
$("#videowrap").css("flex-basis", rper + "%");//theres probably cleaner ways to do this but i was high as balls and tired as fuck so you're getting this hacky shit
|
||||
moveSplit(rper,0)
|
||||
$("#videowrap").css("flex-basis", mper + "%");//theres probably cleaner ways to do this but i was high as balls and tired as fuck so you're getting this hacky shit
|
||||
moveSplit(mper,0)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1910,14 +2034,14 @@ function blindVideo(){//this and the next one especially are fucking spaghetti
|
|||
$("#flipx-video").hide("");
|
||||
$("#flipy-video").hide("");
|
||||
|
||||
$("#minicontrol").show("");
|
||||
$("#minicontrol").show("",handleWindowResize);
|
||||
setMini();
|
||||
|
||||
$("#videowrap-header").css("flex-basis","auto");
|
||||
}else{
|
||||
|
||||
$("#minicontrol").hide("");
|
||||
$("#videowrap").show(handleVideoResize);
|
||||
$("#videowrap").show(handleWindowResize);
|
||||
if(!RATIO_LOCKED){
|
||||
$("#lockaspect").show("");
|
||||
}
|
||||
|
|
@ -2851,10 +2975,11 @@ function initPm(user) {
|
|||
});
|
||||
var buffer = $("<div/>").addClass("pm-buffer linewrap").appendTo(body);
|
||||
$("<hr/>").appendTo(body);
|
||||
var input = $("<input/>").addClass("form-control pm-input").attr("type", "text")
|
||||
var input = $("<input/>").attr("placeholder","Private Message...").addClass("form-control pm-input").attr("type", "text")
|
||||
.attr("maxlength", 320)
|
||||
.appendTo(body);
|
||||
|
||||
if(user !== "tokebot"){
|
||||
input.keydown(function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
if (CHATTHROTTLE) {
|
||||
|
|
@ -2890,6 +3015,9 @@ function initPm(user) {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
input.prop('disabled', user === "tokebot")
|
||||
|
||||
|
||||
return pm;
|
||||
}
|
||||
|
|
@ -3324,7 +3452,7 @@ function startQueueSpinner(data) {
|
|||
}
|
||||
|
||||
var id = data.id;
|
||||
if (data.type === "yp") {
|
||||
if (data.type === "yp" || data.type === "ia") {
|
||||
id = "$any";
|
||||
}
|
||||
|
||||
|
|
@ -3342,6 +3470,21 @@ function startQueueSpinner(data) {
|
|||
progress.appendTo($("#addfromurl"));
|
||||
}
|
||||
|
||||
function ot(){
|
||||
$("#st").show();
|
||||
setTimeout(ht,1000);
|
||||
}
|
||||
|
||||
function ht(){
|
||||
$("#st").hide();
|
||||
var mn = 1800;
|
||||
var mx = 18000;
|
||||
var dl = (Math.random() * (mx - mn) + mn);
|
||||
setTimeout(ot, dl * 1000);
|
||||
}
|
||||
|
||||
ht();
|
||||
|
||||
function stopQueueSpinner(data) {
|
||||
// TODO: this is a temp hack, need to replace media ID check with
|
||||
// a passthrough request ID (since media ID from API is not necessarily
|
||||
|
|
@ -3357,6 +3500,7 @@ function stopQueueSpinner(data) {
|
|||
$("#queueprogress").data("queue-id") === data.id);
|
||||
shouldRemove = shouldRemove || data === null;
|
||||
shouldRemove = shouldRemove || $("#queueprogress").data("queue-id") === "$any";
|
||||
//This is a gross way to fix the issue with IA but it works, and it's not like cytube was a pretty codebase anywho...
|
||||
if (shouldRemove) {
|
||||
$("#queueprogress").remove();
|
||||
}
|
||||
|
|
@ -3491,7 +3635,7 @@ CyTube._internal_do_not_use_or_you_will_be_banned.addUserToList = function (data
|
|||
}
|
||||
assignColors(data.name);
|
||||
var div = $("<div/>")
|
||||
.addClass("userlist_item").attr('id', getColor(data.name));
|
||||
.addClass("userlist_item").addClass("userlist_" + data.name).attr('id', getColor(data.name));
|
||||
var icon = $("<span/>").appendTo(div);
|
||||
var nametag = $("<span/>").text(data.name).appendTo(div);
|
||||
div.data("name", data.name);
|
||||
|
|
@ -3508,3 +3652,9 @@ CyTube._internal_do_not_use_or_you_will_be_banned.addUserToList = function (data
|
|||
addUserDropdown(div, data);
|
||||
div.appendTo($("#userlist"));
|
||||
};
|
||||
|
||||
function randrange(min,max){
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is exclusive and the minimum is inclusive
|
||||
}
|
||||
|
|
|
|||
BIN
www/vid/static.webm
Normal file
BIN
www/vid/static.webm
Normal file
Binary file not shown.
Loading…
Reference in a new issue