Compare commits

..

38 commits

Author SHA1 Message Date
rainbow napkin 0950d34c84 Edit get-info.js 2025-05-05 21:50:37 +00:00
rainbow napkin 649b395fad Added yt-dlp path to config 2025-05-02 04:24:19 -04:00
rainbow napkin 5c104f555a Fixed bugged yt-dlp import. 2025-05-01 06:23:42 -04:00
rainbow napkin ad011c5ece More fixes for youtube... 2025-05-01 06:18:42 -04:00
rainbow napkin 0157262130 Continued youtube improvements. 2025-05-01 06:03:09 -04:00
rainbow napkin bec209c665 Fixed more issues with YTDLP 2025-05-01 05:54:20 -04:00
rainbow napkin 00ac19c80c Fixed broken raw media playback for youtube, added go live button. 2025-05-01 05:35:40 -04:00
rainbownapkin 2acba1a605 Fix broken link grabbing logic. 2024-10-29 22:41:28 -04:00
rainbownapkin 9ba95a073c Somehow this only breaks on certain archives... 2024-10-29 18:59:15 -04:00
rainbownapkin 02dc12e646 Added bulk-queueing from Internet Archive. 2024-10-29 18:44:01 -04:00
forest 9057ed2899 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-10-12 12:13:33 +00:00
forest 46032a33fa Added more tokes 2024-10-12 12:10:20 +00:00
rainbownapkin 1cdf5f8ab9 Emergency Update: Pineapple Express += 2 (1.1.2) 2024-10-12 08:01:30 -04:00
rainbow napkin 05f0eb0048 Update tokes 2023-12-03 21:00:59 +00:00
rainbownapkin 74a555b5f5 re-arrange themes 2023-01-01 01:43:24 +00:00
rainbownapkin c5b000a30a channels will now genrate new bump configs instead of freaking out when there is non 2022-12-04 01:36:43 +00:00
rainbownapkin 2227577138 Fixed useroptions 2022-12-02 04:32:34 +00:00
rainbownapkin cf59498476 XMAS theme 2022-12-02 04:12:55 +00:00
rainbow napkin 8b82c1d9c6 Bust 2022-10-12 00:16:21 +00:00
rainbow napkin 4dcbf09b68 Trim junk off of AGPL 2022-10-10 17:21:00 +00:00
rainbownapkin a6f082d5f1 Hotfix 1:
Critical Bugfix: Users unable to sync due to null reference in fschd.js.
Null checks added, should work fine now.
2022-10-01 12:28:03 +00:00
rainbownapkin 88b98993d2 1.1.1 Final Commit 2022-10-01 04:14:57 +00:00
rainbownapkin 54052d6cfc fixing README.md 2022-08-31 21:17:49 +00:00
rainbownapkin 113cf7900e Adding full patchnotes for v1.1 Pineapple Express 2022-08-31 21:16:21 +00:00
rainbownapkin 15e5513982 Upped version from 1.1-indev to 1.1 on about.pug and READEME.md 2022-08-29 00:48:03 +00:00
rainbownapkin adab2eb3f9 1.1-Indev Final commit before merging to master. Everything seems to
work fine, while being reasonably preformant and pretty.
2022-08-29 00:06:44 +00:00
rainbownapkin 85fbe6bb5a Autobump system and UI complete. The bulk of this update is now over.
Just a couple of bug fixes and tweaks before release.
2022-08-21 21:26:53 +00:00
rainbownapkin c9fb5b0b0e tokebot now resets on tokebumps and every hour on **:19:30. If toke ends
on or after **:19:30 (but before **:20:00) it will extend the toke until
after **:20:00
2022-08-13 07:25:53 +00:00
rainbownapkin cb4c99727f Autobump backend finished. Tweaks may be made during creation of
configuration frontend, but for the most part should be set in stone.
2022-08-13 04:50:09 +00:00
rainbownapkin fcb562397a Finished tokebot implementation. tz and of tokes decapitalized and
consolidated, !r added, toke log and  per user toke count tracking. Toke
count on tooltip. Improved moderator commands & tokebot output
formatting.
2022-07-23 09:24:19 +00:00
rainbownapkin 46bcb040f2 improved tokebot whispers/pm's, mod/admin commands added(reset cooldown,
tokesay/tokeyell/tokewhisper, reloadtoke)
2022-07-22 11:35:37 +00:00
rainbownapkin a048e2094c removed unneeded console.log 2022-07-20 07:26:04 +00:00
rainbownapkin 2fc8f16e9d Tokebot ported to codebase. 2022-07-20 07:22:16 +00:00
rainbownapkin 1da7cab9cd Added mod panel, nested fpanel menus, fpanel playlist 2022-07-19 06:32:22 +00:00
rainbownapkin 3f653c4893 Added raw file youtube support as holdover until invidious embed is
viable. This does not help with region locking, but at least allows
age restricted videos with no sign in, doesnt run any google scripts,
and does not run ads.
2022-07-14 01:54:06 +00:00
rainbownapkin 5535917f08 uncommented some shit I commented that I should not have commented out. 2022-07-12 06:43:55 +00:00
rainbownapkin e171415b30 invidious backend finished. Queueing youtube videos no longer requires
API KEY, does not directly connect to or run any youtube code/servers.
Age Restricted videos are now fully queable.
2022-07-12 06:21:48 +00:00
rainbownapkin f3306f2263 Initial decaf commit. Using bare pre-processed player.js instead of
decaffeinate.js
2022-07-11 00:50:51 +00:00
77 changed files with 61721 additions and 4384 deletions

45
LICENSE
View file

@ -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
View file

@ -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.

View file

@ -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)
);

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
View 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

View file

@ -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: ->

View file

@ -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)

View file

@ -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 shortmedium 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')

View file

@ -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

View file

@ -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 = '&times;'
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 = '&times;'
closeButton.onclick = ->
alertBox.parentNode.removeChild(alertBox)
alertBox.insertBefore(closeButton, alertBox.firstChild)
removeOld($('<div/>').append(alertBox))

View file

@ -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'
}
]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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'
}
]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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 ''

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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"

View file

@ -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
View 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;

View file

@ -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",
@ -515,14 +518,13 @@ 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);
if (user.getName().toLowerCase() === self.ownerName) {
db.channels.updateOwnerLastSeen(self.id);
}
var loginStr = "[login] " + user.displayip + " logged in as " + user.getName();
loginStr += " (aliases: " + user.account.aliases.join(",") + ")";
self.logger.log(loginStr);
self.sendUserJoin(self.users, user);
if (user.getName().toLowerCase() === self.ownerName) {
db.channels.updateOwnerLastSeen(self.id);
}
});
this.users.push(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) {

View file

@ -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,19 +358,25 @@ 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.toLowerCase() in this.commandHandlers) {
this.commandHandlers[cmd.toLowerCase()](user, rcmd, data.meta);
if (data.msg.indexOf("!") === 0) {
return;
}
if (cmd in this.commandHandlers) {
this.commandHandlers[cmd](user, data.msg, data.meta);
return;
}
}
@ -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();

View file

@ -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 */

View file

@ -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
@ -537,17 +543,20 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
type: data.type,
pos: data.pos,
title: data.title,
subtitle: data.subtitle,
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;
}
@ -715,7 +755,10 @@ PlaylistModule.prototype.handleMoveMedia = function (user, data) {
return lock.release();
}
}
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);
self.channel.logger.log("[playlist] " + user.getName() + " moved " +
from.media.title +
(after ? " after " + after.media.title : ""));
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,23 +1061,28 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
}
var qfail = function (msg) {
user.socket.emit("queueFail", {
msg: msg,
link: data.link,
id: data.id
});
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)) {
@ -1051,10 +1105,15 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
}
var usersItems = this.items.findAll(function (item) {
return item.queueby.toLowerCase() === user.getLowerName();
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,11 +1127,14 @@ 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 (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(!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") {
@ -1081,10 +1143,12 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
/* Warn about blocked countries */
if (media.meta.restricted) {
user.socket.emit("queueWarn", {
msg: "Video is blocked in the following countries: " + media.meta.restricted,
link: data.link
});
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, {
@ -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) {
user.socket.emit("errorMsg", {
msg: `Invalid text track error:` + 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);
@ -1240,7 +1307,7 @@ PlaylistModule.prototype.startPlayback = function (time) {
}
/* Lead-in time of 3 seconds to allow clients to buffer */
time = time || (media.seconds > 0 ? -3 : 0);
time = time || (media.seconds > 0 ? -3 : 0);
media.paused = time < 0;
media.currentTime = time;
@ -1260,8 +1327,21 @@ PlaylistModule.prototype.startPlayback = function (time) {
if (!self.current || !self.current.media) {
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
});
self.sendChangeMedia(self.channel.users);
}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
View 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;

View file

@ -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",

View file

@ -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;

View file

@ -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,78 +102,254 @@ 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) {
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");
}
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. 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) {
YouTube.lookup(id).then(function (video) {
var meta = {};
if (video.meta.blocked) {
meta.restricted = video.meta.blocked;
}
if (video.meta.ytRating) {
}
if (video.meta.ytRating) {
meta.ytRating = video.meta.ytRating;
}
var media = new Media(video.id, video.title, video.duration, "yt", meta);
callback(false, media);
}).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(function (err) {
}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) {
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");
}
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. 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) {
videos = videos.map(function (video) {
var meta = {};
if (video.meta.blocked) {
meta.restricted = video.meta.blocked;
YouTube.lookupPlaylist(id).then(function (videos) {
videos = videos.map(function (video) {
var meta = {};
if (video.meta.blocked) {
meta.restricted = video.meta.blocked;
}
return new Media(video.id, video.title, video.duration, "yt", meta);
});
callback(null, videos);
}).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"
}
);
return new Media(video.id, video.title, video.duration, "yt", meta);
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(function (err) {
}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) {
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");
}
ytSearch: async function (query, callback) {
YouTube.search(query).then(function (res) {
var videos = res.results;
videos = videos.map(function (video) {
var meta = {};
if (video.meta.blocked) {
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");
}
YouTube.search(query).then(function (res) {
var videos = res.results;
videos = videos.map(function (video) {
var meta = {};
if (video.meta.blocked) {
meta.restricted = video.meta.blocked;
}
var media = new Media(video.id, video.title, video.duration, "yt", meta);
media.thumb = { url: video.meta.thumbnail };
return media;
});
callback(null, videos);
}).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 media = new Media(video.id, video.title, video.duration, "yt", meta);
media.thumb = { url: video.meta.thumbnail };
return media;
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(function (err) {
}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) {
if(type in this.Getters) {
LOGGER.info("Looking up %s:%s", type, id);
lookupCounter.labels(type).inc(1, new Date());
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);
}
}
} 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);
}
});*/
}
};

View file

@ -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
}
};

View file

@ -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;
}

View file

@ -39,22 +39,21 @@ 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>.
h3 ourfore.st instance rules
ul
li
| Don't be a dick
li
| Don't post, or explain where to find pirated content in the chat
li
| Do not upload content to the internet you do not have permission to for purpose of using it on ourfore.st
li
| 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)
.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 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
| Don't be a dick
li
| Don't post, or explain where to find pirated content in the chat
li
| Do not upload content to the internet you do not have permission to for purpose of using it on ourfore.st
li
| 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 += 3 (v1.1.3)

View file

@ -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,62 +154,63 @@ html(lang="en")
span#plcount 0 items
br
span#pllength 00:00:00
#searchcontrol.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#library_query.form-control(type="text", placeholder="Search query")
span.input-group-btn
button#library_search.btn.btn-default Library
span.input-group-btn
button#youtube_search.btn.btn-default YouTube
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#library.videolist.col-lg-12.col-md-12
#addfromurl.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#mediaurl.form-control(type="text", placeholder="Media URL")
span.input-group-btn
button#queue_next.btn.btn-default Queue next
span.input-group-btn
button#queue_end.btn.btn-default Queue last
span.input-group-btn#showcustomembed
button#showcustomembed.btn.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
span.glyphicon.glyphicon-th-large
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
div#addfromurl-queue
#customembed.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#customembed-title.form-control(type="text", placeholder="Title (optional)")
span.input-group-btn
button#ce_queue_next.btn.btn-default Queue next
span.input-group-btn
button#ce_queue_end.btn.btn-default Queue last
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
| Paste the embed code below and click Next or At End.
| Acceptable embed codes are <code>&lt;iframe&gt;</code> and <code>&lt;object&gt;</code> tags. <strong>CUSTOM EMBEDS CANNOT BE SYNCHRONIZED.</strong>
textarea#customembed-content.input-block-level.form-control(rows="3")
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#userpl_name.form-control(type="text", placeholder="Playlist Name")
span.input-group-btn
button#userpl_save.btn.btn-default Save
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#userpl_list.videolist
#queuefail.col-lg-12.col-md-12
#plmenus
#searchcontrol.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#library_query.form-control(type="text", placeholder="Search query")
span.input-group-btn
button#library_search.btn.btn-default Library
span.input-group-btn
button#youtube_search.btn.btn-default YouTube
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#library.videolist.col-lg-12.col-md-12
#addfromurl.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#mediaurl.form-control(type="text", placeholder="Media URL")
span.input-group-btn
button#queue_next.btn.btn-default Queue next
span.input-group-btn
button#queue_end.btn.btn-default Queue last
span.input-group-btn#showcustomembed
button#showcustomembed.btn.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
span.glyphicon.glyphicon-th-large
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
div#addfromurl-queue
#customembed.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#customembed-title.form-control(type="text", placeholder="Title (optional)")
span.input-group-btn
button#ce_queue_next.btn.btn-default Queue next
span.input-group-btn
button#ce_queue_end.btn.btn-default Queue last
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
| Paste the embed code below and click Next or At End.
| Acceptable embed codes are <code>&lt;iframe&gt;</code> and <code>&lt;object&gt;</code> tags. <strong>CUSTOM EMBEDS CANNOT BE SYNCHRONIZED.</strong>
textarea#customembed-content.input-block-level.form-control(rows="3")
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#userpl_name.form-control(type="text", placeholder="Playlist Name")
span.input-group-btn
button#userpl_save.btn.btn-default Save
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#userpl_list.videolist
#queuefail.col-lg-12.col-md-12
.col-lg-12.col-md-12
ul#queue.videolist
//#leftpane.col-lg-5.col-md-5
@ -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")

View file

@ -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
| &mdash;Install <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank">Tampermonkey</a>.
li
strong Firefox
| &mdash;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
| &mdash;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>.

View file

@ -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

View file

@ -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
View 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

Binary file not shown.

BIN
www/audio/thunder1.ogg Normal file

Binary file not shown.

View file

@ -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;
}

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

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

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

File diff suppressed because it is too large Load diff

BIN
www/img/bumps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
www/img/bumpsdusk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

BIN
www/img/snow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
www/img/xmas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

View file

@ -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';

View file

@ -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
View 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));
}

View file

@ -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))
}

View file

@ -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, '$& ');
}

File diff suppressed because it is too large Load diff

View file

@ -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");
}

View file

@ -41,10 +41,17 @@
Player.prototype.unlatch = function() {
if (!this.seeklatch) {
if (this.latched) {
$("#latchvid").show();
}
return this.latched = false;
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();
}
_this.latched = false;
}
});
} else {
return this.seeklatch = false;
}
@ -242,33 +249,47 @@
}
this.setMediaProperties(data);
this.pauseSeekRaceCondition = false;
waitUntilDefined(window, 'YT', (function(_this) {
return function() {
return waitUntilDefined(YT, 'Player', function() {
var wmode;
removeOld();
wmode = USEROPTS.wmode_transparent ? 'transparent' : 'opaque';
return _this.yt = new YT.Player('ytapiplayer', {
videoId: data.id,
playerVars: {
autohide: 1,
autoplay: 1,
controls: 1,
iv_load_policy: 3,
rel: 0,
wmode: wmode
},
events: {
onReady: _this.onReady.bind(_this),
onStateChange: _this.onStateChange.bind(_this)
}
});
});
};
})(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;
removeOld();
wmode = USEROPTS.wmode_transparent ? 'transparent' : 'opaque';
return _this.yt = new YT.Player('ytapiplayer', {
videoId: data.id,
playerVars: {
autohide: 1,
autoplay: 1,
controls: 1,
iv_load_policy: 3,
rel: 0,
wmode: wmode
},
events: {
onReady: _this.onReady.bind(_this),
onStateChange: _this.onStateChange.bind(_this)
}
});
});
};
})(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,20 +300,30 @@
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) {
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,14 +1766,25 @@
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 (data.type in TYPE_MAP) {
} 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);
} catch (error1) {
@ -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) {

View file

@ -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,8 +342,13 @@ function chatTabComplete(chatline) {
CyTube.chatTabCompleteData.context
);
chatline.value = result.text;
chatline.setSelectionRange(result.newPosition, result.newPosition);
if(hint){
chathint(result.text);
}else{
chathint("");
chatline.value = result.text;
chatline.setSelectionRange(result.newPosition, result.newPosition);
}
}
function callChat(){
@ -301,10 +356,38 @@ function callChat(){
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) {
}
@ -711,8 +815,8 @@ $("#mediaurl").keyup(function(ev) {
$("<input/>").addClass("form-control")//create title field
.attr("type", "text")//the attributes
.attr("id", "addfromurl-title-val")
.attr("placeholder", "Alternate Title")
.attr("style", "display: none; width: 100%;")
.attr("placeholder", "Alternate Title")
.attr("style", "display: none; width: 100%;")
.keydown(function (ev) {
if (ev.keyCode === 13) {
queue("end", "url");
@ -720,17 +824,20 @@ $("#mediaurl").keyup(function(ev) {
})
.appendTo($("#addfromurl-title")).show("blind");//append and show
$("<input/>").addClass("form-control")//create title field
.attr("type", "text")//the attributes
.attr("id", "addfromurl-subtitle-val")
.attr("placeholder", "Alternate Subtitle Track")
.attr("style", "display: none; width: 100%;")
.keydown(function (ev) {
if (ev.keyCode === 13) {
queue("end", "url");
}
})
.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-duration-val")
.attr("placeholder", "Minimum Duration Filter")
.attr("style", "display: none; width: 100%;")
.keydown(function (ev) {
if (ev.keyCode === 13) {
queue("end", "url");
}
})
.appendTo($("#addfromurl-title")).show("blind");//append and show
}
}
} else {
$("#addfromurl-title").hide("blind");
@ -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("&times;")
.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();
};

View file

@ -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,12 +198,13 @@ 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");
.css("top", top + "px");
});
name.mouseleave(function() {
profile.remove();
@ -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,7 +1661,12 @@ function parseMediaLink(url) {
id: url,
type: "cm"
};
} else {
} 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 {
id: url,
@ -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"));
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)
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))
$("#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,45 +2975,49 @@ 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);
input.keydown(function (ev) {
if (ev.keyCode === 13) {
if (CHATTHROTTLE) {
return;
}
var meta = {};
var msg = input.val();
if (msg.trim() === "") {
return;
}
if(user !== "tokebot"){
input.keydown(function (ev) {
if (ev.keyCode === 13) {
if (CHATTHROTTLE) {
return;
}
var meta = {};
var msg = input.val();
if (msg.trim() === "") {
return;
}
if (USEROPTS.modhat && CLIENT.rank >= Rank.Moderator) {
meta.modflair = CLIENT.rank;
}
if (USEROPTS.modhat && CLIENT.rank >= Rank.Moderator) {
meta.modflair = CLIENT.rank;
}
if (CLIENT.rank >= 2 && msg.indexOf("/m ") === 0) {
meta.modflair = CLIENT.rank;
msg = msg.substring(3);
}
socket.emit("pm", {
to: user,
msg: msg,
meta: meta
});
input.val("");
} else if(ev.keyCode == 9) { // Tab completion
try {
chatTabComplete(ev.target);
} catch (error) {
console.error(error);
}
ev.preventDefault();
return false;
}
});
if (CLIENT.rank >= 2 && msg.indexOf("/m ") === 0) {
meta.modflair = CLIENT.rank;
msg = msg.substring(3);
}
socket.emit("pm", {
to: user,
msg: msg,
meta: meta
});
input.val("");
} else if(ev.keyCode == 9) { // Tab completion
try {
chatTabComplete(ev.target);
} catch (error) {
console.error(error);
}
ev.preventDefault();
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,7 +3500,8 @@ function stopQueueSpinner(data) {
$("#queueprogress").data("queue-id") === data.id);
shouldRemove = shouldRemove || data === null;
shouldRemove = shouldRemove || $("#queueprogress").data("queue-id") === "$any";
if (shouldRemove) {
//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

Binary file not shown.