├── .idea ├── .gitignore ├── misc.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── vcs.xml ├── modules.xml └── StripChatRecorderPyUI.iml ├── requirements.txt ├── .gitattributes ├── config.conf ├── run.sh ├── wanted.txt ├── wanted_all.txt ├── README.md ├── Utils.py ├── .gitignore ├── StripchatRecorder.py └── main.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6==6.5.1.1 2 | requests==2.31.0 3 | streamlink==5.5.1 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [paths] 2 | wishlist = ./wanted.txt 3 | save_directory = ./recordings 4 | 5 | [settings] 6 | checkinterval = 20 7 | postprocessingcommand = 8 | postprocessingthreads = 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source venv/bin/activate 3 | python main.py 4 | ``` 5 | 6 | This assumes you're using a Unix-like system (e.g., macOS or Linux) with a virtual environment located in the `venv` directory. If your virtual environment has a different path, make sure to adjust the `source` command accordingly. -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/StripChatRecorderPyUI.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /wanted.txt: -------------------------------------------------------------------------------- 1 | Sweet-QiQi 2 | KCupAnna 3 | Classyfetishrellax 4 | Angel_beibei 5 | Stripchat-yaoyao 6 | yao-yao- 7 | Momogirlhk 8 | Alimolli 9 | akura011 10 | ALICIABIGTITS 11 | No_one_and_you 12 | uh668 13 | yuria1 14 | monika_AAA 15 | Petrichor-66 16 | #UMI_chau 17 | Fcup_KEER 18 | pr3ttyp1nkpussy 19 | Noa_Tachibana 20 | hk_xiaoxiao 21 | ChloeTailor_ 22 | hato_mii 23 | hinakura_lim 24 | Cosmicjenny 25 | Ekoto- 26 | jenni_24 27 | Ecup-JULI 28 | xiaoruan_888 29 | sally_009 30 | Roxy_clark 31 | Linyanyan520 32 | #izumi__li 33 | Candyxtreo 34 | Baby_bunko 35 | #akira_sooup 36 | cinderrella1 37 | #missliiaa 38 | #sakata_ai 39 | teona_ti 40 | yuri_mi 41 | jenny_lii 42 | yoyo_888999 43 | miso_soso_9 44 | anyarosses 45 | -LinLin 46 | nizico_jp 47 | Lovemadeline 48 | ChloeTalor_ 49 | miso_soso_9 50 | Nami_cute 51 | Asuna_G 52 | jasmine-jang -------------------------------------------------------------------------------- /wanted_all.txt: -------------------------------------------------------------------------------- 1 | #Y-baby 2 | #Lisa-Q 3 | Sweet-QiQi 4 | KCupAnna 5 | Sakurachat1218 6 | Classyfetishrellax 7 | Angel_beibei 8 | CnnCooo 9 | Stripchat-yaoyao 10 | yao-yao- 11 | Momogirlhk 12 | Alimolli 13 | akura011 14 | ALICIABIGTITS 15 | uh668 16 | yuria1 17 | monika_AAA 18 | #leilei- 19 | Petrichor-66 20 | #UMI_chau 21 | Fcup_KEER 22 | pr3ttyp1nkpussy 23 | izumi__li 24 | Ariel_Lea 25 | Noa_Tachibana 26 | hk_xiaoxiao 27 | Sexy_Agent 28 | ChloeTailor_ 29 | Cinderrella1 30 | hato_mii 31 | 32 | hinakura_lim 33 | #JunJun-- 34 | Cosmicjenny 35 | yuri_mi 36 | YU-A_chan 37 | -oo-oo- 38 | taeni_lara 39 | -LinLin 40 | Beibei-009 41 | amaya-aki 42 | Y--xinxin 43 | Babimiumiu 44 | Ekoto- 45 | #Office_Assistant 46 | Lover-__- 47 | jenni_24 48 | Eup_Mona 49 | many_0203 50 | papipapi2021 51 | xiaoruan_888 52 | -LinLin 53 | Winnie-009 54 | 55 | #longtime 56 | #izumi__li 57 | Candyxtreo 58 | Baby_bunko 59 | jenni_24 60 | #akira_sooup 61 | cinderrella1 62 | #missliiaa 63 | #sakata_ai 64 | teona_ti 65 | yuri_mi 66 | jenny_lii -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoStrip 2 | 3 | AutoStrip is a Python program used to automatically record and monitor streamers on stripchat.com. It provides details about the current session information of the monitored streamer. 4 | 5 | ## Installation 6 | 7 | To install AutoStrip, follow these steps: 8 | 9 | 1. Download the latest release .zip file from the [Releases](link-to-releases) page. 10 | 2. Unzip the downloaded file to your desired location. 11 | 3. Run the `run.sh` file to start the program. 12 | 13 | That's it! AutoStrip is now installed and ready to use. 14 | 15 | ## Usage 16 | 17 | AutoStrip automatically records and monitors streamers on stripchat.com. It provides information about the ongoing sessions of the monitored streamers. Simply run the program, and it will start monitoring the specified streamers. 18 | 19 | ## Configuration 20 | 21 | Before using AutoStrip, make sure to configure it properly. Open the `config.conf` file in a text editor and set the following parameters: 22 | 23 | - `save_directory`: The directory where recorded videos will be saved. 24 | - `wishlist`: The path to the file containing a list of streamers to monitor. 25 | - `checkInterval`: The interval (in seconds) between each check for new streamer sessions. 26 | - `postProcessingCommand`: The command to be executed after recording a video. (Optional) 27 | 28 | ## Contributing 29 | 30 | Contributions are welcome! If you have any ideas, suggestions, or improvements for AutoStrip, feel free to open an issue or submit a pull request. 31 | 32 | ## Acknowledgement 33 | 34 | This project was inspired by [ChaturbateRecorder](https://github.com/Damianonymous/ChaturbateRecorder/blob/4c76552a97bd39faaedd5f7d00979743c3865278/ChaturbateRecorder.py#L62), a similar tool developed by Damianonymous. 35 | 36 | ## License 37 | 38 | This project is licensed under the [MIT License](link-to-license-file). 39 | -------------------------------------------------------------------------------- /Utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime 4 | import subprocess 5 | 6 | def format_model_to_UI(models): 7 | result = [] 8 | for model in models: 9 | elapsed_time = time.time() - model.start_time 10 | minutes, seconds = divmod(elapsed_time, 60) 11 | start_datetime = datetime.fromtimestamp(model.start_time).strftime('%H:%M:%S') 12 | result.append(f"{model.modelo} | Start From: {start_datetime} ({int(minutes)}m {int(seconds)}s)") 13 | return '\n'.join([f"{result[i]}" for i in range(len(result))]) 14 | 15 | def format_recording_history_to_UI(recording_history): 16 | # Get the model, filename, and isRecording values from the dictionary 17 | model = recording_history["model"] 18 | filename = recording_history["filename"] 19 | status = recording_history["status"] 20 | # Format the dictionary values into the desired format 21 | formatted_string = f"model: {model} | status: {status}\nFile: {filename}" 22 | # Join the formatted strings with a newline separator 23 | return formatted_string 24 | 25 | def add_duration_to_mp4(path, duration): 26 | cmd = f'ffmpeg -i "{path}" -c copy -metadata duration={duration} "{path}_new.mp4"' 27 | subprocess.run(cmd, shell=True) 28 | 29 | def repair_mp4_file(input_file): 30 | output_file = os.path.splitext(input_file)[0] + "_fix.mp4" 31 | vlc_process = subprocess.Popen(["vlc", "-I", "rc", input_file, "--sout", "#transcode{vcodec=h264,acodec=mpga,ab=128,channels=2,samplerate=44100}:standard{access=file,mux=mp4,dst=" + output_file + "}", "vlc://quit"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 32 | 33 | # Return the output file name 34 | vlc_process.wait() 35 | 36 | # Replace the input file with the output file 37 | # os.replace(output_file, input_file) 38 | return output_file 39 | 40 | def repair_mp4_file_ffmpeg(input_file): 41 | # Call ffmpeg to repair the input file 42 | output_file = os.path.splitext(input_file)[0] + "_fix.mp4" 43 | cmd = f'ffmpeg -y -i "{input_file}" -c copy "{output_file}"' 44 | ffmpeg_process = subprocess.run(cmd, shell=True) 45 | 46 | # ffmpeg_process.wait() 47 | os.replace(output_file, input_file) 48 | 49 | return input_file 50 | # Replace the input file with the output file 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | StripchatRecorder.bat 155 | /recordings -------------------------------------------------------------------------------- /StripchatRecorder.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import os 4 | import threading 5 | import sys 6 | import configparser 7 | import subprocess 8 | import queue 9 | import requests 10 | import streamlink 11 | from PySide6 import QtCore, QtWidgets 12 | 13 | import Utils 14 | import tkinter as tk 15 | 16 | if os.name == 'nt': 17 | import ctypes 18 | kernel32 = ctypes.windll.kernel32 19 | kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) 20 | 21 | # mainDir = sys.path[0] 22 | Config = configparser.ConfigParser() 23 | setting = {} 24 | # processingQueue = queue.Queue() 25 | recording = [] 26 | postprocessing = [] 27 | runProg = True 28 | hilos = [] 29 | recording_history = [] 30 | 31 | 32 | def cls(): 33 | os.system('cls' if os.name == 'nt' else 'clear') 34 | 35 | def readConfig(): 36 | global setting 37 | if getattr(sys, 'frozen', False): 38 | # Running as an executable (pyinstaller-generated) 39 | mainDir = os.path.dirname(sys.executable) 40 | else: 41 | # Running in an IDE or as a script 42 | mainDir = os.path.dirname(os.path.realpath(sys.argv[0])) 43 | Config.read(mainDir + '/config.conf') 44 | setting = { 45 | 'save_directory': Config.get('paths', 'save_directory'), 46 | 'wishlist': Config.get('paths', 'wishlist'), 47 | 'interval': int(Config.get('settings', 'checkInterval')), 48 | 'postProcessingCommand': Config.get('settings', 'postProcessingCommand'), 49 | } 50 | try: 51 | setting['postProcessingThreads'] = int(Config.get('settings', 'postProcessingThreads')) 52 | except ValueError: 53 | if setting['postProcessingCommand'] and not setting['postProcessingThreads']: 54 | setting['postProcessingThreads'] = 1 55 | 56 | if not os.path.exists(f'{setting["save_directory"]}'): 57 | os.makedirs(f'{setting["save_directory"]}') 58 | 59 | # def postProcess(): 60 | # while runProg is True: 61 | # while processingQueue.empty(): 62 | # time.sleep(1) 63 | # parameters = processingQueue.get() 64 | # print("PostProcess") 65 | # model = parameters['model'] 66 | # path = parameters['path'] 67 | # filename = os.path.split(path)[-1] 68 | # directory = os.path.dirname(path) 69 | # file = os.path.splitext(filename)[0] 70 | # subprocess.call(setting['postProcessingCommand'].split() + [path, filename, directory, model, file, 'cam4']) 71 | 72 | class Modelo(threading.Thread): 73 | def __init__(self, modelo): 74 | super().__init__() 75 | self.modelo = modelo 76 | self._stopevent = threading.Event() 77 | self.file = None 78 | self.online = None 79 | self.lock = threading.Lock() 80 | self.start_time = None 81 | self.stop_time = None 82 | self.repair_thread = None 83 | 84 | def run(self): 85 | global recording, hilos, recording_history 86 | isOnline = self.isOnline() 87 | if isOnline == False: 88 | self.online = False 89 | else: 90 | self.online = True 91 | self.file = os.path.join(setting['save_directory'], self.modelo, f'{datetime.datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d_%H.%M.%S")}_{self.modelo}.mp4') 92 | try: 93 | if self.start_time is None: 94 | self.start_time = time.time() 95 | print(self.start_time, isOnline) 96 | session = streamlink.Streamlink() 97 | streams = session.streams(f'hlsvariant://{isOnline}') 98 | print("Model: " + self.modelo + " is online") 99 | stream = streams['best'] 100 | fd = stream.open() 101 | 102 | if not isModelInListofObjects(self.modelo, recording): 103 | os.makedirs(os.path.join(setting['save_directory'], self.modelo), exist_ok=True) 104 | with open(self.file, 'wb') as f: 105 | self.lock.acquire() 106 | recording.append(self) 107 | recording_history.append({"model": self.modelo, "filename": self.file, "status": "Recording"}) 108 | print(self.modelo + " added to recording history") 109 | for index, hilo in enumerate(hilos): 110 | if hilo.modelo == self.modelo: 111 | del hilos[index] 112 | break 113 | self.lock.release() 114 | while not (self._stopevent.isSet() or os.fstat(f.fileno()).st_nlink == 0): 115 | try: 116 | data = fd.read(1024) 117 | f.write(data) 118 | # Utils.add_duration_to_mp4(self.file, time.time() - self.start_time) 119 | # print("WRITE DURATION: "+self.modelo + time.time() - self.start_time) 120 | except: 121 | fd.close() 122 | break 123 | # if setting['postProcessingCommand']: 124 | # processingQueue.put({'model': self.modelo, 'path': self.file}) 125 | except Exception as e: 126 | with open('log.log', 'a+') as f: 127 | f.write(f'\n{datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")} EXCEPTION: {e}\n') 128 | self.stop() 129 | finally: 130 | self.exceptionHandler() 131 | 132 | def exceptionHandler(self): 133 | self.stop() 134 | self.online = False 135 | self.lock.acquire() 136 | for index, hilo in enumerate(recording): 137 | if hilo.modelo == self.modelo: 138 | del recording[index] 139 | matching_item = next((item for item in recording_history if item["model"] == self.modelo), None) 140 | matching_item["status"] = "Stopped Recording" 141 | print("RECORDING HISTORY: " + '\n'.join([f"{recording_history[i]}" for i in range(len(recording_history))])) 142 | break 143 | self.lock.release() 144 | try: 145 | file = os.path.join(os.getcwd(), self.file) 146 | if os.path.isfile(file): 147 | if os.path.getsize(file) <= 1024: 148 | os.remove(file) 149 | 150 | except Exception as e: 151 | with open('log.log', 'a+') as f: 152 | f.write(f'\n{datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")} EXCEPTION: {e}\n') 153 | def isOnline(self): 154 | try: 155 | resp = requests.get(f'https://stripchat.com/api/front/v2/models/username/{self.modelo}/cam').json() 156 | hls_url = '' 157 | if 'cam' in resp.keys(): 158 | if resp['cam']['viewServers'] and resp['cam']['isCamAvailable'] and resp['cam']['streamName']: 159 | if 'flashphoner-hls' in resp['cam']['viewServers'].keys(): 160 | hls_url = f'https://b-{resp["cam"]["viewServers"]["flashphoner-hls"]}.doppiocdn.com/hls/{resp["cam"]["streamName"]}/{resp["cam"]["streamName"]}.m3u8' 161 | print(hls_url) 162 | if len(hls_url): 163 | return hls_url 164 | else: 165 | return False 166 | except: 167 | return False 168 | 169 | def stop(self): 170 | global recording, hilos 171 | self._stopevent.set() 172 | if isModelInListofObjects(self.modelo, recording): 173 | print("STOP MODEL: " + self.modelo + "self.file: " + self.file) 174 | if not (self.file in postprocessing): 175 | postprocessing.append(self.file) 176 | print(postprocessing) 177 | #self.repair_thread = self.repair_mp4_file_thread() 178 | 179 | 180 | class CleaningThread(threading.Thread): 181 | def __init__(self): 182 | super().__init__() 183 | self.interval = 0 184 | self.lock = threading.Lock() 185 | 186 | def run(self): 187 | global hilos, recording, runProg 188 | while runProg is True: 189 | self.lock.acquire() 190 | new_hilos = [] 191 | for hilo in hilos: 192 | if hilo.is_alive() or hilo.online: 193 | new_hilos.append(hilo) 194 | # Get the new elements (i.e. elements in new_array but not in old_array) 195 | new_model = list(set(new_hilos) - set(hilos)) 196 | 197 | # Get the removed elements (i.e. elements in old_array but not in new_array) 198 | removed_model = list(set(hilos) - set(new_hilos)) 199 | 200 | # Print the new and removed elements 201 | print(f"New elements: {new_model[i]}" for i in range(len(new_model))) 202 | print(f"Removed elements: {removed_model[i]}" for i in range(len(removed_model))) 203 | hilos = new_hilos 204 | self.lock.release() 205 | for i in range(10, 0, -1): 206 | self.interval = i 207 | time.sleep(1) 208 | 209 | 210 | 211 | 212 | class AddModelsThread(threading.Thread): 213 | def __init__(self): 214 | super().__init__() 215 | self.wanted = [] 216 | self.lock = threading.Lock() 217 | self.repeatedModels = [] 218 | self.counterModel = 0 219 | 220 | def run(self): 221 | global hilos, recording 222 | lines = open(setting['wishlist'], 'r').read().splitlines() 223 | self.wanted = (x for x in lines if x) 224 | self.lock.acquire() 225 | aux = [] 226 | for model in self.wanted: 227 | model = model.lower() 228 | if model in aux: 229 | self.repeatedModels.append(model) 230 | else: 231 | aux.append(model) 232 | self.counterModel = self.counterModel + 1 233 | if not isModelInListofObjects(model, hilos) and not isModelInListofObjects(model, recording): 234 | thread = Modelo(model) 235 | thread.start() 236 | # print("APPEND MODEL: " + model) 237 | hilos.append(thread) 238 | for hilo in recording: 239 | if hilo.modelo not in aux: 240 | hilo.stop() 241 | self.lock.release() 242 | 243 | def isModelInListofObjects(obj, lista): 244 | result = False 245 | for i in lista: 246 | if i.modelo == obj: 247 | result = True 248 | break 249 | return result 250 | 251 | def stopRecording(): 252 | global runProg 253 | runProg = False 254 | for hilo in recording: 255 | hilo.stop() 256 | 257 | # class ChatRecorder(QtCore.QObject): 258 | # messageSignal = QtCore.Signal(str) 259 | # 260 | # def __init__(self): 261 | # super().__init__() 262 | # self.stop_event = threading.Event() 263 | # self.recordingList = [] 264 | # def startRecording(recordingList): 265 | # global runProg, recording_history 266 | # readConfig() 267 | # runProg = True 268 | # cleaningThread = CleaningThread() 269 | # cleaningThread.start() 270 | # 271 | # while runProg is True: 272 | # readConfig() 273 | # addModelsThread = AddModelsThread() 274 | # addModelsThread.start() 275 | # i = 1 276 | # for i in range(setting['interval'], 0, -1): 277 | # cls() 278 | # if (runProg is True): 279 | # recordingList[0].clear() 280 | # recordingList[1].clear() 281 | # if len(addModelsThread.repeatedModels): print( 282 | # 'The following models are more than once in wanted: [\'' + ', '.join( 283 | # modelo for modelo in addModelsThread.repeatedModels) + '\']') 284 | # print( 285 | # f'{len(hilos):02d} alive Threads (1 Thread per non-recording model), cleaning dead/not-online Threads in {cleaningThread.interval:02d} seconds, {addModelsThread.counterModel:02d} models in wanted') 286 | # print(f'Online Threads (models): {len(recording):02d}') 287 | # print('The following models are being recorded:') 288 | # for hiloModelo in recording: 289 | # print(f' Model: {hiloModelo.modelo} --> File: {os.path.basename(hiloModelo.file)}') 290 | # recordingList[0].append(hiloModelo) 291 | # for history in recording_history: 292 | # recordingList[1].append(history) 293 | # print("inside recordng list", recordingList[0]) 294 | # print("inside recordng history", recordingList[1]) 295 | # print(f'Next check in {i:02d} seconds') 296 | # 297 | # time.sleep(1) 298 | # else: 299 | # 300 | # recordingList[0].clear() 301 | # recordingList[1].clear() 302 | # addModelsThread.join() 303 | # 304 | # del addModelsThread, i 305 | # print("START POSTPROCESSING: " + '\n'.join([f"{postprocessing[i]}" for i in range(len(postprocessing))])) 306 | # 307 | # for file in postprocessing: 308 | # print("START REPAIR THREAD") 309 | # 310 | # def target(): 311 | # Utils.repair_mp4_file_ffmpeg(file) 312 | # 313 | # # output_file = Utils.repair_mp4_file_ffmpeg(file) 314 | # thread = threading.Thread(target=target) 315 | # thread.daemon = True 316 | # thread.start() 317 | # postprocessing.clear() 318 | # 319 | # # Create a new thread and start it 320 | # # thread = threading.Thread(target=target) 321 | # # thread.daemon = True 322 | # # thread.start() 323 | # cleaningThread.join() 324 | # # processingQueue.join() 325 | # exit() 326 | # 327 | # def stopRecording(self): 328 | # self.stop_event.set() 329 | 330 | def startRecording(recordingList): 331 | global runProg, recording_history 332 | readConfig() 333 | runProg = True 334 | cleaningThread = CleaningThread() 335 | cleaningThread.start() 336 | 337 | while runProg is True: 338 | readConfig() 339 | addModelsThread = AddModelsThread() 340 | addModelsThread.start() 341 | i = 1 342 | for i in range(setting['interval'], 0, -1): 343 | cls() 344 | if (runProg is True): 345 | recordingList[0].clear() 346 | recordingList[1].clear() 347 | if len(addModelsThread.repeatedModels): print('The following models are more than once in wanted: [\'' + ', '.join(modelo for modelo in addModelsThread.repeatedModels) + '\']') 348 | print(f'{len(hilos):02d} alive Threads (1 Thread per non-recording model), cleaning dead/not-online Threads in {cleaningThread.interval:02d} seconds, {addModelsThread.counterModel:02d} models in wanted') 349 | print(f'Online Threads (models): {len(recording):02d}') 350 | print('The following models are being recorded:') 351 | for hiloModelo in recording: 352 | print(f' Model: {hiloModelo.modelo} --> File: {os.path.basename(hiloModelo.file)}') 353 | recordingList[0].append(hiloModelo) 354 | for history in recording_history: 355 | recordingList[1].append(history) 356 | print("inside recordng list", recordingList[0]) 357 | print("inside recordng history", recordingList[1]) 358 | print(f'Next check in {i:02d} seconds') 359 | 360 | time.sleep(1) 361 | else: 362 | 363 | recordingList[0].clear() 364 | recordingList[1].clear() 365 | addModelsThread.join() 366 | 367 | del addModelsThread, i 368 | print("START POSTPROCESSING: " + '\n'.join([f"{postprocessing[i]}" for i in range(len(postprocessing))])) 369 | 370 | for file in postprocessing: 371 | print("START REPAIR THREAD") 372 | 373 | def target(): 374 | Utils.repair_mp4_file_ffmpeg(file) 375 | # output_file = Utils.repair_mp4_file_ffmpeg(file) 376 | thread = threading.Thread(target=target) 377 | thread.daemon = True 378 | thread.start() 379 | postprocessing.clear() 380 | 381 | # Create a new thread and start it 382 | # thread = threading.Thread(target=target) 383 | # thread.daemon = True 384 | # thread.start() 385 | cleaningThread.join() 386 | # processingQueue.join() 387 | exit() 388 | 389 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # This is a sample Python script. 2 | import sys 3 | import threading 4 | import time 5 | 6 | from PySide6.QtCore import Qt 7 | from PySide6.QtWidgets import QLabel, QFileDialog 8 | import configparser 9 | import StripchatRecorder 10 | from StripchatRecorder import startRecording 11 | from PySide6 import QtCore, QtWidgets 12 | import Utils 13 | # Press Shift+F10 to execute it or replace it with your code. 14 | # Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. 15 | 16 | 17 | class StripchatUI(QtWidgets.QMainWindow): 18 | def __init__(self): 19 | super().__init__() 20 | self.setWindowTitle("StripchatRecorder") 21 | self.selectedFiles = [] 22 | self.setMinimumSize(800, 500) 23 | self.runProg = True 24 | # Create tabs 25 | self.recList = [[],[]] 26 | self.tabs = QtWidgets.QTabWidget() 27 | self.recThread = threading.Thread(target=StripchatRecorder.startRecording, args=(self.recList,)) 28 | # Create Tab 1 29 | self.tab1 = QtWidgets.QWidget() 30 | self.tab1Layout = QtWidgets.QHBoxLayout(self.tab1) 31 | self.Config = configparser.ConfigParser() 32 | # Create Left Panel 33 | self.leftPanel = QtWidgets.QVBoxLayout() 34 | self.mainDir = sys.path[0] 35 | self.Config.read(self.mainDir + '/config.conf') 36 | self.wanted_model = open(self.Config.get('paths', 'wishlist'), 'r').read().splitlines() 37 | 38 | # Create the layout and add the first QLineEdit widget 39 | self.lineEditsLayout = QtWidgets.QVBoxLayout() 40 | self.lineEdits = [QtWidgets.QLineEdit() for _ in range(len(self.wanted_model)+1)] 41 | # print(len(self.wanted_model)) 42 | 43 | self.lineEditsWidget = QtWidgets.QWidget() 44 | self.lineEditsVbox = QtWidgets.QVBoxLayout() 45 | self.lineEditsWidget.setLayout(self.lineEditsVbox) 46 | self.lineEditsScrollArea = QtWidgets.QScrollArea() 47 | self.lineEditsScrollArea.setWidget(self.lineEditsWidget) 48 | self.lineEditsScrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 49 | self.lineEditsScrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 50 | self.lineEditsScrollArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 51 | self.lineEditsScrollArea.setWidgetResizable(True) 52 | self.lineEditsScrollArea.setStyleSheet("QWidget {border: 1px solid gray; border-radius: 5px;}") 53 | 54 | # Create the button to add new QLineEdit widgets 55 | self.addButton = QtWidgets.QPushButton("Add Model") 56 | self.addButton.clicked.connect(self.addLineEdit) 57 | # self.addButton.setAlignment(Qt.AlignmentFlag.AlignBottom) 58 | # Create a widget to hold the line edits layout 59 | # Create a scroll area to hold the line edits widget 60 | 61 | # Add the scroll area to the left panel 62 | self.leftPanel.addWidget(self.lineEditsScrollArea) 63 | 64 | for i in (range(len(self.lineEdits))): 65 | if i < len(self.wanted_model): 66 | # print(i) 67 | self.lineEdits[i].setText(self.wanted_model[i]) 68 | self.lineEditsVbox.addWidget(self.lineEdits[i]) 69 | 70 | 71 | # Add the button to the left panel 72 | # self.leftPanel.addWidget(self.addButton, alignment=QtCore.Qt.AlignBottom) 73 | # Create TextInput Box 74 | self.inputStream = QtWidgets.QLineEdit() 75 | self.inputStream.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 76 | 77 | # Create Start Button 78 | self.startButton = QtWidgets.QPushButton("Start") 79 | self.startButton.clicked.connect(self.startRecording) 80 | 81 | # Create Stop Button 82 | self.stopButton = QtWidgets.QPushButton("Stop") 83 | self.stopButton.clicked.connect(self.stopRecording) 84 | 85 | self.applyModelsButton = QtWidgets.QPushButton("Apply Changes") 86 | self.applyModelsButton.clicked.connect(self.applyModel) 87 | # self.stopButton.clicked.connect(self.stopRecording) 88 | 89 | # Add TextInput Box and Start Button to Left Panel 90 | # self.leftPanel.addWidget(self.inputStream, 2) 91 | self.tab1ActionRows = QtWidgets.QHBoxLayout() 92 | self.tab1InputRows = QtWidgets.QHBoxLayout() 93 | self.tab1InputRows.addWidget(self.addButton, 1) 94 | self.tab1InputRows.addWidget(self.applyModelsButton, 1) 95 | self.tab1ActionRows.addWidget(self.startButton, 1) 96 | self.tab1ActionRows.addWidget(self.stopButton, 1) 97 | self.leftPanel.addLayout(self.tab1InputRows, 1) 98 | self.leftPanel.addLayout(self.tab1ActionRows, 1) 99 | # self.leftPanel.addWidget(self.startButton, 1) 100 | 101 | # Create Right Panel 102 | self.rightPanel = QtWidgets.QVBoxLayout() 103 | 104 | # Create Bordered Textbox 105 | self.streamerBox = QtWidgets.QGroupBox("Recording Streamer") 106 | self.streamerBox.setStyleSheet("QGroupBox {border: 1px solid gray; border-radius: 5px; padding-top: 10px}") 107 | 108 | # Create Label 109 | self.streamerLabel = QtWidgets.QLabel("No streamer selected") 110 | self.streamerLabel.setAlignment(QtCore.Qt.AlignTop) 111 | 112 | self.recordingBox = QtWidgets.QWidget() 113 | self.recordingBox.setStyleSheet("QWidget {border: 1px solid gray; border-radius: 5px; padding-top: 10px}") 114 | self.recordingBoxLayout = QtWidgets.QVBoxLayout() 115 | self.recordingBox.setLayout(self.recordingBoxLayout) 116 | 117 | # self.recordingHistory = QtWidgets.QLabel("Recording History") 118 | # self.recordingHistory.setAlignment(QtCore.Qt.AlignTop) 119 | 120 | # Add Label to Bordered Textbox 121 | self.streamerLayout = QtWidgets.QVBoxLayout(self.streamerBox) 122 | self.streamerLayout.addWidget(self.streamerLabel) 123 | 124 | self.streamerDisplayWidget = QtWidgets.QWidget() 125 | self.streamerDisplayVbox = QtWidgets.QVBoxLayout() 126 | self.streamerDisplayWidget.setLayout(self.streamerDisplayVbox) 127 | self.recordingScrollArea = QtWidgets.QScrollArea(self.recordingBox) 128 | self.recordingScrollArea.setWidget(self.streamerDisplayWidget) 129 | self.recordingScrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 130 | self.recordingScrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 131 | # self.recordingScrollArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 132 | self.recordingScrollArea.setWidgetResizable(True) 133 | self.recordingScrollArea.setStyleSheet("QWidget {border: 1px solid gray; border-radius: 5px;}") 134 | # Add Bordered Textbox to Right Panel 135 | self.rightPanel.addWidget(self.streamerBox, 1) 136 | self.rightPanel.addWidget(self.recordingScrollArea, 1) 137 | 138 | # Add Left and Right Panel to Tab 1 139 | self.tab1Layout.addLayout(self.leftPanel, 1) 140 | self.tab1Layout.addLayout(self.rightPanel, 1) 141 | 142 | self.fixVideoTabWidget = QtWidgets.QWidget() 143 | self.fixVideoTabLayout = QtWidgets.QHBoxLayout() 144 | self.fixVideoTabWidget.setLayout(self.fixVideoTabLayout) 145 | 146 | self.addFileBtn = QtWidgets.QPushButton("Select Files") 147 | # self.btn.clicked.connect(self.getfile) 148 | 149 | self.flineEditsWidget = QtWidgets.QWidget() 150 | self.flineEditsVbox = QtWidgets.QVBoxLayout() 151 | self.flineEditsVbox.setAlignment(Qt.AlignmentFlag.AlignTop) 152 | self.flineEditsWidget.setLayout(self.flineEditsVbox) 153 | self.flineEditsScrollArea = QtWidgets.QScrollArea() 154 | self.flineEditsScrollArea.setWidget(self.flineEditsWidget) 155 | self.flineEditsScrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 156 | self.flineEditsScrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 157 | self.flineEditsScrollArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 158 | self.flineEditsScrollArea.setWidgetResizable(True) 159 | self.flineEditsScrollArea.setStyleSheet("QWidget {border: 1px solid gray; border-radius: 5px;}") 160 | 161 | self.fAddButton = QtWidgets.QPushButton("Select Video") 162 | self.fAddButton.clicked.connect(self.getfiles) 163 | 164 | self.fClearButton = QtWidgets.QPushButton("Clear Selection") 165 | self.fClearButton.clicked.connect(self.clearSelection) 166 | 167 | self.fRunButton = QtWidgets.QPushButton("Start Fix") 168 | self.fRunButton.clicked.connect(self.startFix) 169 | 170 | self.fixVideoButtonWidget1 = QtWidgets.QWidget() 171 | self.fixVideoButtonLayout1 = QtWidgets.QHBoxLayout() 172 | self.fixVideoButtonWidget1.setLayout(self.fixVideoButtonLayout1) 173 | self.fixVideoButtonLayout1.addWidget(self.fAddButton) 174 | self.fixVideoButtonLayout1.addWidget(self.fClearButton) 175 | 176 | self.fixVideoButtonWidget2 = QtWidgets.QWidget() 177 | self.fixVideoButtonLayout2 = QtWidgets.QHBoxLayout() 178 | self.fixVideoButtonWidget2.setLayout(self.fixVideoButtonLayout2) 179 | self.fixVideoButtonLayout2.addWidget(self.fRunButton) 180 | 181 | self.PPLeftPanelWidget = QtWidgets.QWidget() 182 | self.PPLeftPanelLayout = QtWidgets.QVBoxLayout() 183 | self.PPLeftPanelLayout.addWidget(self.flineEditsScrollArea) 184 | self.PPLeftPanelLayout.addWidget(self.fixVideoButtonWidget1) 185 | self.PPLeftPanelLayout.addWidget(self.fixVideoButtonWidget2) 186 | # self.PPLeftPanelWidget.setStyleSheet("background-color: yellow") 187 | self.PPLeftPanelWidget.setLayout(self.PPLeftPanelLayout) 188 | 189 | self.PPRightPanelWidget = QtWidgets.QWidget() 190 | self.PPRightPanelLayout = QtWidgets.QVBoxLayout() 191 | # self.PPRightPanelWidget.setStyleSheet("background-color: red") 192 | self.PPRightPanelWidget.setLayout(self.PPRightPanelLayout) 193 | 194 | 195 | self.fixVideoTabLayout.addWidget(self.PPLeftPanelWidget, 1) 196 | self.fixVideoTabLayout.addWidget(self.PPRightPanelWidget, 1) 197 | 198 | 199 | self.tabSetting = QtWidgets.QWidget() 200 | self.tabSettingLayout = QtWidgets.QVBoxLayout() 201 | self.tabFormLayout = QtWidgets.QFormLayout() 202 | self.tabSetting.setLayout(self.tabSettingLayout) 203 | 204 | self.targetDirTextEdit = QtWidgets.QLineEdit() 205 | self.targetDirTextEdit.setText(self.Config.get('paths', 'save_directory')) 206 | self.wantedModelDirTextEdit = QtWidgets.QLineEdit() 207 | self.wantedModelDirTextEdit.setText(self.Config.get('paths', 'wishlist')) 208 | 209 | self.tabFormLayout.addRow("Save Recording Directory", self.targetDirTextEdit) 210 | self.tabFormLayout.addRow("Wanted Model File Directory", self.wantedModelDirTextEdit) 211 | 212 | self.applySetting = QtWidgets.QPushButton("Apply") 213 | self.applySetting.clicked.connect(self.applyConfig) 214 | self.settingActionRow = QtWidgets.QHBoxLayout(); 215 | self.settingActionRow.addWidget(self.applySetting) 216 | self.tabSettingLayout.addLayout(self.tabFormLayout) 217 | self.tabSettingLayout.addLayout(self.settingActionRow) 218 | # Add Tab 1 to the tabs 219 | self.tabs.addTab(self.tab1, "Recording") 220 | self.tabs.addTab(self.fixVideoTabWidget, "Fix Videos") 221 | self.tabs.addTab(self.tabSetting, "Setting") 222 | 223 | 224 | # Add tabs to the main window 225 | self.setCentralWidget(self.tabs) 226 | 227 | # Set window properties 228 | self.setWindowTitle("StripchatUI") 229 | self.setGeometry(100, 100, 800, 600) 230 | 231 | # Update the text every second 232 | self.timer = QtCore.QTimer() 233 | self.timer.timeout.connect(self.updateUI) 234 | self.timer.start(1000) 235 | 236 | def addLineEdit(self): 237 | # Add a new QLineEdit widget to the list and layout 238 | newLineEdit = QtWidgets.QLineEdit() 239 | self.lineEdits.append(newLineEdit) 240 | self.lineEditsVbox.addWidget(newLineEdit) 241 | # for wid in self.lineEdits: 242 | # wid.setStyleSheet("background-color: yellow") 243 | 244 | def addSelectedFile(self, filename): 245 | # Add a new QLineEdit widget to the list and layout 246 | newLineEdit = QtWidgets.QLineEdit() 247 | newLineEdit.setText(filename) 248 | self.selectedFiles.append(filename) 249 | self.fixSelectedFileListLayout.addWidget(newLineEdit) 250 | # for wid in self.lineEdits: 251 | # wid.setStyleSheet("background-color: yellow") 252 | 253 | def applyModel(self): 254 | with open(self.Config.get('paths', 'wishlist'), 'w') as file: 255 | file.write('\n'.join(i.text() for i in self.lineEdits)) 256 | 257 | def applyConfig(self): 258 | self.Config.set('paths', 'wishlist', self.wantedModelDirTextEdit.text()) 259 | self.Config.set('paths', 'save_directory', self.targetDirTextEdit.text()) 260 | with open(self.mainDir + '/config.conf', 'w') as configfile: 261 | self.Config.write(configfile) 262 | def updateUI(self): 263 | print(Utils.format_model_to_UI(self.recList[0])) 264 | self.streamerLabel.setText(Utils.format_model_to_UI(self.recList[0])) 265 | 266 | for i in reversed(range(self.streamerDisplayVbox.count())): 267 | self.streamerDisplayVbox.itemAt(i).widget().setParent(None) 268 | 269 | for history in self.recList[1]: 270 | label = QLabel(Utils.format_recording_history_to_UI(history)) 271 | label.setAlignment(QtCore.Qt.AlignTop) 272 | if history["status"] == "Stopped Recording": 273 | label.setStyleSheet("background-color: rgba(168, 0, 0, 204);") 274 | self.streamerDisplayVbox.addWidget(label) 275 | # self.recordingHistory.setText(Utils.format_recording_history_to_UI(self.recList[1])) 276 | 277 | def startRecording(self): 278 | self.runProg = True 279 | self.recThread.start() 280 | 281 | def getfiles(self): 282 | dlg = QFileDialog() 283 | dlg.setFileMode(QFileDialog.FileMode.ExistingFiles) 284 | dlg.setMimeTypeFilters( 285 | { 286 | "video/mp4" 287 | } 288 | ) 289 | filenames = [] 290 | 291 | if dlg.exec_(): 292 | filenames = dlg.selectedFiles() 293 | for file in filenames: 294 | if file not in self.selectedFiles: 295 | self.selectedFiles.append(file) 296 | 297 | for i in reversed(range(self.flineEditsVbox.count())): 298 | self.flineEditsVbox.itemAt(i).widget().setParent(None) 299 | for filename in self.selectedFiles: 300 | label = QLabel(filename) 301 | label.setAlignment(QtCore.Qt.AlignTop) 302 | delButton = QtWidgets.QPushButton("Remove") 303 | delButton.clicked.connect(self.addLineEdit) 304 | selectedFileItemWidget = QtWidgets.QWidget() 305 | selectedFileItemLayout = QtWidgets.QHBoxLayout() 306 | selectedFileItemLayout.addWidget(label) 307 | selectedFileItemLayout.addWidget(delButton) 308 | selectedFileItemWidget.setLayout(selectedFileItemLayout) 309 | # selectedFileItemWidget.setStyleSheet("background-color: yellow") 310 | 311 | newLineEdit = QtWidgets.QLineEdit() 312 | newLineEdit.setText(filename) 313 | newLineEdit.setEnabled(False) 314 | self.flineEditsVbox.addWidget(newLineEdit) 315 | 316 | #self.fixSelectedFileListLayout.addWidget(selectedFileItemWidget) 317 | print(filename) 318 | 319 | # newLineEdit = QtWidgets.QLineEdit() 320 | # self.lineEdits.append(newLineEdit) 321 | # self.lineEditsVbox.addWidget(newLineEdit) 322 | def startFix(self): 323 | print("Start Fixing") 324 | for file in self.selectedFiles: 325 | print("START REPAIR THREAD") 326 | 327 | def target(): 328 | Utils.repair_mp4_file_ffmpeg(file) 329 | 330 | # output_file = Utils.repair_mp4_file_ffmpeg(file) 331 | thread = threading.Thread(target=target) 332 | thread.daemon = True 333 | thread.start() 334 | def clearSelection(self): 335 | print("Clear Selection") 336 | self.selectedFiles.clear() 337 | def stopRecording(self): 338 | print("STOP") 339 | StripchatRecorder.stopRecording() 340 | self.recThread = threading.Thread(target=StripchatRecorder.startRecording, args=(self.recList,)) 341 | 342 | # Press the green button in the gutter to run the script. 343 | if __name__ == '__main__': 344 | # recList = [[]] 345 | # Start recording on a separate thread 346 | # recThread = threading.Thread(target=startRecording, args=(recList,)) 347 | # recThread.start() 348 | # while True: 349 | # print("reclist: ", recList[0]) 350 | # time.sleep(1) 351 | app = QtWidgets.QApplication([]) 352 | app.setStyle('Fusion') 353 | ui = StripchatUI() 354 | ui.show() 355 | app.exec_() 356 | # See PyCharm help at https://www.jetbrains.com/help/pycharm/ 357 | --------------------------------------------------------------------------------