├── .gitignore ├── README.md ├── config.py ├── play.py ├── player ├── __init__.py ├── gstreamer.py ├── vlc.py └── vlcbind │ └── vlc.py ├── updater ├── __init__.py └── updater.py └── webm └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | webm/** 3 | webm/!.gitkeep 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Бесконечный Сосач 2 | 3 | **Смотрите WebM-треды с 2ch.hk без перерыва** 4 | Этот скрипт ищет WebM-треды на популярной российской имиджборде 2ch.hk (ранее находящейся по адресу 2ch.so, сосач) и воспроизводит все найденные видеофайлы. 5 | Обычная и детская порнография, Джон Сина, Everybody knows shit's fucked, † CΛIN † - ama ama ama, спины с Primo Victoria, kokovin93, Имя 505, Алесандр Пистолетов, Зеленый Слоник, My Little Pony, корейские PV и MV, теперь на вашем десктопе, _бесконечно_! Все воспроизводимые файлы можно сохранять в директорию. 6 | 7 | ### Зависимости 8 | * Python 3 9 | * Gstreamer 1.0 + python-gst или VLC 10 | * LADSPA с swh-plugins для компрессора и лимитера (чтобы успокоить ДЖОНА СИНУ) 11 | * Requests 12 | 13 | ### Управление 14 | * **s** для пропуска видео 15 | * **d** для пропуска 10 видео 16 | * **q** или **Escape** для выхода 17 | * **f** чтобы перейти в полноэкранный режим 18 | * **c** чтобы скопировать ссылку в буфер 19 | * **Space** для паузы 20 | 21 | ### Как использовать 22 | Скопируйте User-Agent и cookie "cf_clearance" из своего браузера в config.py и запустите play.py. Наслаждайтесь! 23 | 24 | -------------------------------------------- 25 | 26 | # Endless Sosuch 27 | 28 | **Never stop watching WebM threads from 2ch.hk.** 29 | This script searches for WebM threads on a popular Russian imageboard 2ch.hk (formely 2ch.so, sosuch) and plays all the video files found. 30 | Regular and child porn, John Cena, Everybody knows shit's fucked, † CΛIN † - ama ama ama, Primo Victoria spinnings, kokovin93, Imya 505, Alexander Pistoletov, Green Elephant, My Little Pony, Korean PVs and MVs now on your desktop, _endlessly_! All played files could be saved in a directory. 31 | 32 | ### Requirements: 33 | * Python 3 34 | * Gstreamer 1.0 + python-gst or VLC 35 | * LADSPA with swh-plugins for compressor and limiter (to calm down JOHN CENA) 36 | * Requests 37 | 38 | ### Controls 39 | * **s** to **s**kip video 40 | * **d** to skip 10 videos 41 | * **q** or **Escape** to **q**uit 42 | * **f** to go **f**ullscreen 43 | * **c** to copy link into buffer 44 | * **Space** to **pause** 45 | 46 | ### How to use 47 | Copy User-Agent and "cf_clearance" cookie into config.py and run play.py. Enjoy! 48 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Path to directory with WebM files 4 | RANDOM_PATH = 'webm' 5 | 6 | # Should we save all played to the end files to that directory? 7 | SAVE_FILES = True 8 | 9 | # Keywords in an OP's post to search for 10 | INCLUDE_KEYWORDS = r'(?i)(([WEBM]|[ЦУИЬ])|([ВШ][ЕБМ]))' 11 | #INCLUDE_KEYWORDS = None 12 | 13 | # Keywords to exclude 14 | #EXCLUDE_KEYWORDS = r'(?i)((анимублядский)|(порн))' 15 | EXCLUDE_KEYWORDS = None 16 | 17 | # Cloudflare "cf_clearance" cookie value 18 | # Currently supported only by gstreamer backend 19 | CF_COOKIE = '861a0566de1421f863f81936700d70e6f9d15356-1444513397-604800' 20 | 21 | # Your User-Agent for that cookie 22 | CF_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 Firefox/41.0' 23 | 24 | # Backend to use 25 | # 'gstreamer' or 'vlc' 26 | BACKEND = 'gstreamer' 27 | 28 | # Use audio compressor and limiter to normalize volume level between different video files 29 | # Requires LADSPA with swh-plugins for gstreamer 30 | # Uses compressor and volnorm in VLC (works worse than in gstreamer) 31 | AUDIO_COMPRESSOR = False 32 | 33 | # Select GStreamer sinks that work for you 34 | # See: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good-plugins/html/ 35 | GSTREAMER_VIDEO_SINK = 'autovideosink' 36 | GSTREAMER_AUDIO_SINK = 'autoaudiosink' 37 | 38 | # Use gstreamer buffering 39 | # Generally works fine but sometimes could stale a stream 40 | GSTREAMER_BUFFERING = True 41 | 42 | # Additional GStreamer pipeline. Can be used to stream video to the remote server 43 | # In order to use it, you should create two queues with names "vq" and "aq" 44 | #GSTREAMER_ADDITIONAL_PIPELINE = 'queue name=vq ! fakesink queue name=aq ! fakesink' 45 | GSTREAMER_ADDITIONAL_PIPELINE = None 46 | 47 | # Select VLC sinks 48 | VLC_AUDIO_SINK = None 49 | VLC_VIDEO_SINK = None 50 | -------------------------------------------------------------------------------- /play.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import updater.updater 4 | import signal 5 | import logging 6 | import config 7 | import re 8 | 9 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 10 | signal.signal(signal.SIGINT, signal.SIG_DFL) 11 | logger = logging.getLogger('main') 12 | 13 | if config.BACKEND == 'gstreamer': 14 | import player.gstreamer 15 | player = player.gstreamer.Player( 16 | config.RANDOM_PATH if config.SAVE_FILES else None, 17 | config.AUDIO_COMPRESSOR, 18 | config.GSTREAMER_VIDEO_SINK, 19 | config.GSTREAMER_AUDIO_SINK, 20 | config.GSTREAMER_ADDITIONAL_PIPELINE, 21 | config.GSTREAMER_BUFFERING 22 | ) 23 | elif config.BACKEND == 'vlc': 24 | import player.vlc 25 | player = player.vlc.Player( 26 | config.RANDOM_PATH if config.SAVE_FILES else None, 27 | config.AUDIO_COMPRESSOR, 28 | config.VLC_VIDEO_SINK, 29 | config.VLC_AUDIO_SINK 30 | ) 31 | else: 32 | logger.error('No working backend set!') 33 | quit() 34 | 35 | player.set_random_directory(config.RANDOM_PATH) 36 | 37 | player.cookie = config.CF_COOKIE 38 | player.user_agent = config.CF_USER_AGENT 39 | 40 | board = updater.updater.Board() 41 | board.req.cookies.set('cf_clearance', config.CF_COOKIE) 42 | board.req.headers['User-Agent'] = config.CF_USER_AGENT 43 | 44 | try: 45 | compiled_include_keywords = re.compile(config.INCLUDE_KEYWORDS) 46 | compiled_exclude_keywords = re.compile(config.EXCLUDE_KEYWORDS) if config.EXCLUDE_KEYWORDS else '' 47 | except (ConfigurationError, AttributeError, TypeError, NameError): 48 | logger.fatal("You should set both INCLUDE_KEYWORDS and EXCLUDE_KEYWORDS in the configuration file!") 49 | quit(1) 50 | 51 | def on_empty_queue(): 52 | logger.info('Queue is empty, updating') 53 | board.update() 54 | board.find_threads(compiled_include_keywords, compiled_exclude_keywords) 55 | board.parse_threads() 56 | for video in board.get_new_videos_list(): 57 | logger.debug('Got video {}'.format(video)) 58 | player.videoqueue.put(video) 59 | 60 | player.register_on_video_queue_empty_callback(on_empty_queue) 61 | on_empty_queue() 62 | 63 | if __name__ == '__main__': 64 | player.run() 65 | -------------------------------------------------------------------------------- /player/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /player/gstreamer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import gi 3 | gi.require_version('Gtk', '3.0') 4 | gi.require_version('GdkX11', '3.0') 5 | gi.require_version('GstVideo', '1.0') 6 | gi.require_version('Gst', '1.0') 7 | from gi.repository import GObject, GLib, Gst, Gtk, Gdk 8 | from gi.repository import GdkX11, GstVideo 9 | import queue 10 | import os 11 | import os.path 12 | import random 13 | import time 14 | import logging 15 | import glob 16 | import sys 17 | import ctypes 18 | 19 | GObject.threads_init() 20 | Gst.init(None) 21 | 22 | class NoDirectoryException(Exception): 23 | pass 24 | 25 | class Player(object): 26 | cursor_none = Gdk.Cursor.new(Gdk.CursorType.BLANK_CURSOR) 27 | cursor_left = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR) 28 | if sys.platform == 'linux': 29 | ctypes.cdll.LoadLibrary('libX11.so').XInitThreads() 30 | 31 | def __init__(self, file_save_dir=False, use_compressor=False, video_sink='autovideosink', 32 | audio_sink='autoaudiosink', add_sink=None, buffering=True): 33 | self.logger = logging.getLogger('video') 34 | self.window = Gtk.Window() 35 | self.window.connect('destroy', self.quit) 36 | self.window.set_default_size(800, 450) 37 | self.window.set_title('Endless Sosuch') 38 | 39 | self.window.connect("key-release-event", self.on_key_release) 40 | 41 | self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 42 | 43 | # Create GStreamer pipeline 44 | self.pipeline = Gst.Pipeline() 45 | 46 | # Create bus to get events from GStreamer pipeline 47 | self.bus = self.pipeline.get_bus() 48 | self.bus.add_signal_watch() 49 | self.bus.connect('message::eos', self.on_eos) 50 | self.bus.connect('message::error', self.on_error) 51 | self.bus.connect('message::buffering', self.on_buffering) 52 | 53 | # This is needed to make the video output in our DrawingArea: 54 | self.bus.enable_sync_message_emission() 55 | self.bus.connect('sync-message::element', self.on_sync_message) 56 | 57 | # Add video queue 58 | self.videoqueue = queue.Queue() 59 | self.randomdir = None 60 | self.file_save_dir = file_save_dir 61 | self.use_compressor = use_compressor 62 | self.video_sink = video_sink 63 | self.audio_sink = audio_sink 64 | self.add_sink = add_sink 65 | self.window_is_fullscreen = False 66 | self.is_paused = True 67 | self.uri = None 68 | self.empty_queue_callback = None 69 | self.user_agent = None 70 | self.cookie = None 71 | self.buffering = buffering 72 | 73 | self.build_pipeline() 74 | 75 | def build_pipeline(self): 76 | # Create GStreamer elements 77 | self.videobin = Gst.parse_bin_from_description('queue max-size-buffers=0 max-size-bytes=0 max-size-time=1000000000 ! ' 78 | + self.video_sink, True) 79 | self.audiobin = Gst.parse_bin_from_description('queue max-size-buffers=0 max-size-bytes=0 max-size-time=1000000000 ! \ 80 | audioconvert name=audiosink ! ' + \ 81 | ('ladspa-sc4-1882-so-sc4 ratio=5 attack-time=5 release-time=120 threshold-level=-10 ! \ 82 | ladspa-fast-lookahead-limiter-1913-so-fastlookaheadlimiter input-gain=10 limit=-3 ! ' if self.use_compressor 83 | else 'queue max-size-buffers=0 max-size-bytes=0 max-size-time=1000000000 ! ') \ 84 | + self.audio_sink, True) 85 | self.decodebin = Gst.ElementFactory.make('decodebin', 'dec') 86 | self.audioconvert_tee = Gst.ElementFactory.make('audioconvert', 'audioconvert_tee') 87 | self.videoconvert_tee = Gst.ElementFactory.make('videoconvert', 'videoconvert_tee') 88 | self.audiotee = Gst.ElementFactory.make('tee', 'audiotee') 89 | self.videotee = Gst.ElementFactory.make('tee', 'videotee') 90 | if self.add_sink: 91 | self.add_pipeline = Gst.parse_bin_from_description(self.add_sink, False) 92 | self.pipeline.add(self.add_pipeline) 93 | 94 | # Add everything to the pipeline 95 | self.pipeline.add(self.decodebin) 96 | self.pipeline.add(self.audioconvert_tee) 97 | self.pipeline.add(self.videoconvert_tee) 98 | self.pipeline.add(self.audiotee) 99 | self.pipeline.add(self.videotee) 100 | 101 | self.audioconvert_tee.link(self.audiotee) 102 | self.videoconvert_tee.link(self.videotee) 103 | 104 | self.decodebin.connect('pad-added', self.on_pad_added) 105 | self.decodebin.connect('no-more-pads', self.on_no_more_pads) 106 | 107 | def reinit_pipeline(self, uri): 108 | if self.pipeline.get_by_name('tee'): 109 | self.pipeline.remove(self.tee_queue) 110 | if self.pipeline.get_by_name('uri'): 111 | self.pipeline.remove(self.source) 112 | if self.pipeline.get_by_name('filesink'): 113 | self.pipeline.remove(self.filesink) 114 | 115 | if 'http://' in uri or 'https://' in uri: 116 | self.source = Gst.ElementFactory.make('souphttpsrc' ,'uri') 117 | self.source.set_property('user-agent', self.user_agent) if self.user_agent else None 118 | self.source.set_property('cookies', ['cf_clearance=' + self.cookie]) if self.cookie else None 119 | 120 | if self.file_save_dir and not os.path.isfile(self.file_save_dir + '/' + os.path.basename(uri)): 121 | self.tee_queue = Gst.parse_bin_from_description('tee name=tee \ 122 | tee. ! queue name=filequeue \ 123 | tee. ! queue2 name=decodequeue use-buffering=true', False) 124 | self.filesink = Gst.ElementFactory.make('filesink' ,'filesink') 125 | self.filesink.set_property('location', self.file_save_dir + '/' + os.path.basename(uri)) 126 | self.filesink.set_property('async', False) 127 | else: 128 | self.tee_queue = Gst.parse_bin_from_description('tee name=tee ! queue2 name=decodequeue use-buffering=true', False) 129 | self.filesink = None 130 | else: 131 | self.tee_queue = Gst.parse_bin_from_description('tee name=tee ! queue2 name=decodequeue use-buffering=true', False) 132 | self.source = Gst.ElementFactory.make('filesrc' ,'uri') 133 | self.filesink = None 134 | 135 | self.pipeline.add(self.tee_queue) 136 | self.pipeline.get_by_name('decodequeue').link(self.decodebin) 137 | self.pipeline.add(self.source) 138 | if self.filesink: 139 | self.pipeline.add(self.filesink) 140 | self.pipeline.get_by_name('filequeue').link(self.filesink) 141 | self.source.link(self.pipeline.get_by_name('tee')) 142 | self.source.set_property('location', uri) 143 | 144 | self.has_audio = False 145 | self.has_video = False 146 | 147 | def seturi(self, uri): 148 | if not uri: 149 | return 150 | self.reinit_pipeline(uri) 151 | self.uri = uri 152 | self.update_titlebar() 153 | 154 | def run(self): 155 | self.window.show_all() 156 | # You need to get the XID after window.show_all(). You shouldn't get it 157 | # in the on_sync_message() handler because threading issues will cause 158 | # segfaults there. 159 | videowindow = self.window.get_window() 160 | if sys.platform == 'win32': 161 | ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p 162 | ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] 163 | drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(videowindow.__gpointer__, None) 164 | gdkdll = ctypes.CDLL ("libgdk-3-0.dll") 165 | self.xid = gdkdll.gdk_win32_window_get_handle(drawingarea_gpointer) 166 | else: 167 | self.xid = videowindow.get_xid() 168 | self.seturi(self.get_queued_or_random()) 169 | self.play() 170 | Gtk.main() 171 | 172 | def play(self): 173 | if not self.is_paused: 174 | return 175 | 176 | self.pipeline.set_state(Gst.State.PLAYING) 177 | self.logger.info('Playing {}'.format(self.uri)) 178 | self.is_paused = False 179 | GObject.timeout_add_seconds(1, self.update_titlebar) 180 | 181 | def pause(self): 182 | if self.is_paused: 183 | return 184 | 185 | self.pipeline.set_state(Gst.State.PAUSED) 186 | self.is_paused = True 187 | 188 | def stop(self, should_delete=False): 189 | location = None 190 | if should_delete: 191 | try: 192 | location = self.filesink.get_property('location') 193 | except: 194 | pass 195 | 196 | self.pipeline.set_state(Gst.State.NULL) 197 | if location: 198 | os.remove(location) 199 | self.is_paused = True 200 | 201 | def quit(self, window = None): 202 | self.stop(True) 203 | Gtk.main_quit() 204 | 205 | def copy_to_clipboard(self, textdata): 206 | self.clipboard.set_text(textdata, -1) 207 | 208 | def set_random_directory(self, randomdir): 209 | self.randomdir = randomdir 210 | 211 | def add_queue(self, item): 212 | self.videoqueue.put(item) 213 | 214 | def register_on_video_queue_empty_callback(self, callback): 215 | self.empty_queue_callback = callback 216 | 217 | def get_random(self): 218 | if self.randomdir is None: 219 | raise NoDirectoryException('Directory path is not set!') 220 | try: 221 | return random.choice(glob.glob(os.path.abspath(self.randomdir) + '/*.webm')) 222 | except IndexError: 223 | self.logger.error('Directory with random files is empty!') 224 | 225 | def get_queued_or_random(self): 226 | try: 227 | video = self.videoqueue.get_nowait() 228 | except queue.Empty: 229 | # get random video from folder 230 | video = self.get_random() 231 | if self.empty_queue_callback: 232 | self.empty_queue_callback() 233 | return video 234 | 235 | def toggle_fullscreen(self): 236 | if not self.window_is_fullscreen: 237 | self.window.fullscreen() 238 | self.window.get_window().set_cursor(self.cursor_none) 239 | self.window_is_fullscreen = True 240 | else: 241 | self.window.unfullscreen() 242 | self.window.get_window().set_cursor(self.cursor_left) 243 | self.window_is_fullscreen = False 244 | 245 | def toggle_play(self): 246 | if not self.is_paused: 247 | self.pause() 248 | else: 249 | self.play() 250 | 251 | def update_titlebar(self): 252 | time_str = None 253 | try: 254 | dur = self.pipeline.query_duration(Gst.Format.TIME)[1] 255 | pos = self.pipeline.query_position(Gst.Format.TIME)[1] 256 | 257 | time_str = '%d:%.2d / %d:%.2d' % ( 258 | int(float(pos) / Gst.SECOND) // 60, 259 | int(float(pos) / Gst.SECOND) % 60, 260 | int(float(dur) / Gst.SECOND) // 60, 261 | int(float(dur) / Gst.SECOND) % 60 262 | ) 263 | except: 264 | pass 265 | 266 | self.window.set_title('Endless Sosuch | ' + 267 | os.path.basename(self.uri) + 268 | (' | ' + time_str if time_str else '')) 269 | 270 | return not self.is_paused 271 | 272 | def link_video(self, element=None, pad=None): 273 | if not self.pipeline.get_by_name(self.videobin.get_name()): 274 | self.pipeline.add(self.videobin) 275 | if element and pad: 276 | # If we have an element with a pad, we link exact pad to the 277 | # video output. If not, link any pad from the decoder. 278 | element.link_pads(pad.get_name(), self.videoconvert_tee, None) 279 | else: 280 | self.decodebin.link(self.videoconvert_tee) 281 | self.videotee.link(self.videobin) 282 | if self.add_sink: 283 | self.videotee.link(self.pipeline.get_by_name('vq')) 284 | self.videobin.sync_state_with_parent() 285 | 286 | def on_sync_message(self, bus, msg): 287 | if msg.get_structure().get_name() == 'prepare-window-handle': 288 | self.logger.debug('prepare-window-handle') 289 | msg.src.set_window_handle(self.xid) 290 | 291 | def on_buffering(self, bus, msg): 292 | buf = msg.parse_buffering() 293 | if self.buffering: 294 | if buf < 20: 295 | self.pause() 296 | elif buf >= 80: 297 | self.play() 298 | 299 | def on_pad_added(self, element, pad): 300 | string = pad.query_caps(None).to_string() 301 | self.logger.debug('Pad added: {}'.format(string)) 302 | event = pad.get_sticky_event(Gst.EventType.STREAM_START, 0) 303 | stream_flags = event.parse_stream_flags().value_names 304 | if string.startswith('audio/'): 305 | self.has_audio = True 306 | if not self.pipeline.get_by_name(self.audiobin.get_name()): 307 | self.pipeline.add(self.audiobin) 308 | self.decodebin.link(self.audioconvert_tee) 309 | self.audiotee.link(self.audiobin) 310 | if self.add_sink: 311 | self.audiotee.link(self.pipeline.get_by_name('aq')) 312 | self.audiobin.sync_state_with_parent() 313 | if string.startswith('video/'): 314 | if self.has_video or ('GST_STREAM_FLAG_SELECT' not in stream_flags and not self.has_video): 315 | # Not linking a video stream without default flag (probably a preview) or if it's not 316 | # a first video stream 317 | self.logger.debug('Not linking stream with flags {}'.format(stream_flags)) 318 | return False 319 | self.has_video = True 320 | self.link_video(element, pad) 321 | 322 | def on_no_more_pads(self, element): 323 | self.logger.debug('No more pads') 324 | if not self.has_audio and self.add_sink: 325 | # Can't handle it since additional pipeline always assumes audio 326 | GLib.idle_add(self.on_eos, 0) 327 | elif not self.has_audio: 328 | self.pipeline.remove(self.audiobin) 329 | if not self.has_video: 330 | # A workaround for a bit wrongly muxed video files with no default flag on a video stream. 331 | # If we haven't linked a video in on_pad_added and there are no other video streams, we should 332 | # re-link it to the output. 333 | self.logger.info('Wrongly muxed video file detected. Re-linking video output.') 334 | self.has_video = True 335 | self.link_video() 336 | 337 | def on_eos(self, bus=None, msg=None): 338 | self.logger.debug('on_eos()') 339 | self.stop(not(bus)) 340 | uri = self.get_queued_or_random() 341 | if uri: 342 | self.seturi(uri) 343 | self.play() 344 | else: 345 | self.on_eos() 346 | 347 | def on_error(self, bus, msg): 348 | self.logger.error('on_error(): {}'.format(msg.parse_error())) 349 | time.sleep(1) 350 | self.on_eos() 351 | 352 | def on_key_release(self, window, ev, data=None): 353 | keyval = Gdk.keyval_to_lower(ev.keyval) 354 | if keyval == Gdk.KEY_s: 355 | self.on_eos() 356 | if keyval == Gdk.KEY_d: 357 | for i in range(9): 358 | try: 359 | self.videoqueue.get_nowait() 360 | except queue.Empty: 361 | break 362 | self.on_eos() 363 | # get random video from folder 364 | elif keyval in (Gdk.KEY_Escape, Gdk.KEY_q): 365 | self.quit() 366 | elif keyval == Gdk.KEY_f: 367 | self.toggle_fullscreen() 368 | elif keyval == Gdk.KEY_c: 369 | self.copy_to_clipboard(self.uri) 370 | elif keyval == Gdk.KEY_space: 371 | self.toggle_play() 372 | -------------------------------------------------------------------------------- /player/vlc.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import gi 3 | gi.require_version('Gst', '1.0') 4 | from gi.repository import GObject, Gst, Gtk, Gdk 5 | from gi.repository import GdkX11 6 | import queue 7 | import os 8 | import sys 9 | import os.path 10 | import random 11 | import time 12 | import logging 13 | import glob 14 | import player.vlcbind.vlc as vlc 15 | import ctypes 16 | import threading 17 | 18 | GObject.threads_init() 19 | Gst.init(None) 20 | 21 | class NoDirectoryException(Exception): 22 | pass 23 | 24 | 25 | class Player(object): 26 | def __init__(self, file_save_dir=None, use_compressor=False, video_sink=None, audio_sink=None): 27 | self.logger = logging.getLogger('video') 28 | self.window = Gtk.Window() 29 | self.window.connect('destroy', self.quit) 30 | self.window.set_default_size(800, 450) 31 | self.window.set_title('Endless Sosuch') 32 | 33 | self.drawingarea = Gtk.DrawingArea() 34 | self.window.add(self.drawingarea) 35 | self.window.connect("key-release-event", self.on_key_release) 36 | 37 | if sys.platform == 'linux': 38 | self.x11 = ctypes.cdll.LoadLibrary('libX11.so') 39 | self.x11.XInitThreads() 40 | 41 | self.instance = vlc.Instance('--sout-mux-caching 100 ' + 42 | ('--vout ' + video_sink if video_sink else '') + 43 | ('--aout ' + audio_sink if audio_sink else '') + 44 | ('--audio-filter compress,volnorm --compressor-ratio 5 --compressor-threshold -10 --norm-max-level -3' if use_compressor else '') 45 | ) 46 | self.vlc = self.instance.media_player_new() 47 | 48 | # Add video queue 49 | self.videoqueue = queue.Queue() 50 | self.randomdir = None 51 | self.file_save_dir = file_save_dir 52 | self.use_compressor = use_compressor 53 | self.video_sink = video_sink 54 | self.audio_sink = audio_sink 55 | self.window_is_fullscreen = False 56 | self.is_paused = True 57 | self.uri = None 58 | self.empty_queue_callback = None 59 | self.user_agent = None 60 | self.cookie = None 61 | self.thread_queue = queue.Queue() 62 | self.thread = threading.Thread(target=self.on_eos_thread).start() 63 | 64 | def seturi(self, uri): 65 | media = self.instance.media_new(uri) 66 | if ('http://' in uri or 'https://' in uri) and self.file_save_dir \ 67 | and not os.path.isfile(self.file_save_dir + '/' + os.path.basename(uri)): 68 | media.add_option(':sout=#duplicate{dst=display,dst=std{access=file,dst="' +\ 69 | self.file_save_dir + '/' + os.path.basename(uri) + '"}}') 70 | 71 | self.vlc.set_media(media) 72 | self.window.set_title('Endless Sosuch | ' + os.path.basename(uri)) 73 | self.uri = uri 74 | 75 | def run(self): 76 | self.window.show_all() 77 | # You need to get the XID after window.show_all(). You shouldn't get it 78 | # in the on_sync_message() handler because threading issues will cause 79 | # segfaults there. 80 | videowindow = self.drawingarea.get_property('window') 81 | if sys.platform == 'win32': 82 | ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p 83 | ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] 84 | drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(videowindow.__gpointer__, None) 85 | gdkdll = ctypes.CDLL ("libgdk-3-0.dll") 86 | self.xid = gdkdll.gdk_win32_window_get_handle(drawingarea_gpointer) 87 | self.vlc.set_hwnd(self.xid) 88 | else: 89 | self.xid = videowindow.get_xid() 90 | self.vlc.set_xwindow(self.xid) 91 | 92 | self.vlc.event_manager().event_attach(vlc.EventType.MediaPlayerEndReached, self.on_eos, 1) 93 | self.vlc.event_manager().event_attach(vlc.EventType.MediaPlayerEncounteredError, self.on_error, 1) 94 | self.instance.set_user_agent('http', self.user_agent) 95 | self.seturi(self.get_queued_or_random()) 96 | self.play() 97 | 98 | Gtk.main() 99 | 100 | def play(self): 101 | if not self.is_paused: 102 | return 103 | 104 | self.vlc.play() 105 | self.logger.info('Playing {}'.format(self.uri)) 106 | self.is_paused = False 107 | 108 | def pause(self): 109 | if self.is_paused: 110 | return 111 | 112 | self.vlc.pause() 113 | self.is_paused = True 114 | 115 | def stop(self, should_delete=False): 116 | self.vlc.stop() 117 | self.is_paused = True 118 | 119 | if should_delete and ('http://' in self.uri or 'https://' in self.uri) \ 120 | and self.file_save_dir: 121 | os.remove(self.file_save_dir + '/' + os.path.basename(self.uri)) 122 | 123 | def quit(self, window = None): 124 | self.stop(True) 125 | self.thread_queue.put(False) 126 | self.vlc.release() 127 | self.instance.release() 128 | Gtk.main_quit() 129 | 130 | def set_random_directory(self, randomdir): 131 | self.randomdir = randomdir 132 | 133 | def add_queue(self, item): 134 | self.videoqueue.put(item) 135 | 136 | def register_on_video_queue_empty_callback(self, callback): 137 | self.empty_queue_callback = callback 138 | 139 | def get_random(self): 140 | if self.randomdir is None: 141 | raise NoDirectoryException('Directory path is not set!') 142 | try: 143 | return random.choice(glob.glob(os.path.abspath(self.randomdir) + '/*.webm')) 144 | except IndexError: 145 | self.logger.error('Directory with random files is empty!') 146 | 147 | def get_queued_or_random(self): 148 | try: 149 | video = self.videoqueue.get_nowait() 150 | except queue.Empty: 151 | # get random video from folder 152 | video = self.get_random() 153 | if self.empty_queue_callback: 154 | self.empty_queue_callback() 155 | return video 156 | 157 | def toggle_fullscreen(self): 158 | if not self.window_is_fullscreen: 159 | self.window.fullscreen() 160 | self.window_is_fullscreen = True 161 | else: 162 | self.window.unfullscreen() 163 | self.window_is_fullscreen = False 164 | #self.window.resize(*self.window.get_size()) 165 | self.window.show_all() 166 | 167 | def toggle_play(self): 168 | if not self.is_paused: 169 | self.pause() 170 | else: 171 | self.play() 172 | 173 | def on_eos_thread(self): 174 | while True: 175 | message = self.thread_queue.get() 176 | if message: 177 | self.stop() 178 | uri = self.get_queued_or_random() 179 | self.seturi(uri) 180 | self.play() 181 | else: 182 | self.logger.info('Quitting thread') 183 | break 184 | 185 | def on_eos(self, bus=None, msg=None): 186 | self.logger.debug('on_eos()') 187 | if bus or msg: 188 | self.thread_queue.put(True) 189 | else: 190 | self.stop(True) 191 | uri = self.get_queued_or_random() 192 | self.seturi(uri) 193 | self.play() 194 | 195 | def on_error(self, bus, msg): 196 | self.logger.error('on_error(): {}'.format(msg.parse_error())) 197 | time.sleep(1) 198 | self.thread_queue.put(True) 199 | 200 | def on_key_release(self, window, ev, data=None): 201 | if ev.keyval == Gdk.KEY_s or ev.keyval == Gdk.KEY_S: 202 | self.on_eos() 203 | if ev.keyval == Gdk.KEY_d or ev.keyval == Gdk.KEY_d: 204 | for i in range(9): 205 | try: 206 | self.videoqueue.get_nowait() 207 | except queue.Empty: 208 | break 209 | self.on_eos() 210 | # get random video from folder 211 | elif ev.keyval == Gdk.KEY_Escape or ev.keyval == Gdk.KEY_q or ev.keyval == Gdk.KEY_Q: 212 | self.quit() 213 | elif ev.keyval == Gdk.KEY_f or ev.keyval == Gdk.KEY_F: 214 | self.toggle_fullscreen() 215 | elif ev.keyval == Gdk.KEY_space: 216 | self.toggle_play() 217 | -------------------------------------------------------------------------------- /updater/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /updater/updater.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import requests 3 | import json 4 | import re 5 | import logging 6 | import sys 7 | 8 | URL = 'https://2ch.hk/b/index.json' 9 | BASEURL = 'https://2ch.hk' 10 | BASEURL_BOARD = 'https://2ch.hk/b' 11 | if sys.platform == 'win32': 12 | # GnuTLS crashes on HTTPS, dunno why. 13 | BASEURL = 'http://2ch.hk/b' 14 | BASEURL_BOARD = 'http://2ch.hk/b' 15 | 16 | class Thread(object): 17 | def __init__(self, url=None): 18 | self.logger = logging.getLogger('thread') 19 | self.url = url 20 | self.data = None 21 | self.videos = list() 22 | self.lastupdated = None 23 | self.old_latest_video_index = -1 24 | self.latest_video_index = -1 25 | 26 | def __str__(self): 27 | return self.url 28 | 29 | def __repr__(self): 30 | return self.__str__() 31 | 32 | def __eq__(self, other): 33 | return self.url == other.url 34 | 35 | def download(self, requests_session = None): 36 | if requests_session: 37 | req = requests_session 38 | else: 39 | req = requests.Session() 40 | 41 | self.data = req.get(BASEURL_BOARD + self.url) 42 | if self.data.status_code != requests.codes.ok: 43 | self.logger.info('Thread is unavailable') 44 | return False 45 | return True 46 | 47 | def parsevideos(self): 48 | self.videos.clear() 49 | 50 | try: 51 | parser = json.loads(self.data.text) 52 | except: 53 | self.logger.error('Cannot parse thread JSON') 54 | return 55 | 56 | for post in parser['threads'][0]['posts']: 57 | for postfile in post['files']: 58 | if '.webm' in postfile['path']: 59 | webm = BASEURL + postfile['path'] 60 | self.videos.append(webm) 61 | 62 | self.logger.info("New videos: {}".format(len(self.videos) - self.latest_video_index)) 63 | self.old_latest_video_index = self.latest_video_index 64 | self.latest_video_index = len(self.videos) 65 | 66 | def get_new_videos_list(self): 67 | ret = list() 68 | self.logger.debug('old_latest {}'.format(self.old_latest_video_index)) 69 | for i, video in enumerate(self.videos): 70 | if i > self.old_latest_video_index: 71 | ret.append(video) 72 | self.old_latest_video_index = self.latest_video_index 73 | return ret 74 | 75 | 76 | class Board(object): 77 | def __init__(self): 78 | self.logger = logging.getLogger('board') 79 | self.req = requests.Session() 80 | self.threads = list() 81 | self.data = None 82 | 83 | def update(self): 84 | self.data = self.req.get(URL) 85 | 86 | def find_threads(self, include=r'([Ww][Ee][Bb][Mm])|([Цц][Уу][Ии][Ьь])|([ВвШш][Ее][Бб][Мм])', 87 | exclude=None): 88 | try: 89 | parser = json.loads(self.data.text) 90 | except: 91 | self.logger.error('Cannot parse board JSON') 92 | return 93 | 94 | for thread in parser['threads']: 95 | url = '/res/' + thread['posts'][0]['num'] + '.json' 96 | body = thread['posts'][0]['comment'] 97 | is_webm = any(['webm' in file['path'] for file in thread['posts'][0]['files']]) 98 | if is_webm and re.search(include, body) and not (re.search(exclude, body) if exclude else False): 99 | if Thread(url) not in self.threads: 100 | self.logger.info('Found new Webm thread: {}'.format(BASEURL_BOARD + url)) 101 | self.threads.append(Thread(url)) 102 | if not self.threads: 103 | self.logger.info('No threads found!') 104 | 105 | def parse_threads(self): 106 | for thread in self.threads: 107 | if not thread.download(self.req): 108 | self.logger.debug('removing thread') 109 | self.threads.remove(thread) 110 | else: 111 | self.logger.debug('parsing thread') 112 | thread.parsevideos() 113 | 114 | def get_new_videos_list(self): 115 | ret = list() 116 | for thread in self.threads: 117 | for video in thread.get_new_videos_list(): 118 | ret.append(video) 119 | return ret 120 | -------------------------------------------------------------------------------- /webm/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ValdikSS/endless-sosuch/6efe883c106451f58302a442554f0a0ee56bccd5/webm/.gitkeep --------------------------------------------------------------------------------