├── .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 |
4 |
5 |
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 |
12 |
13 |
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 |
--------------------------------------------------------------------------------