Commit
·
fde6ce9
1
Parent(s):
c602fc5
xd
Browse files- app.py +289 -31
- auth.py +227 -0
- config.toml +75 -0
- cookies.txt +31 -0
- proxy_auth_extension/__init__.py +0 -0
- proxy_auth_extension/__pycache__/__init__.cpython-310.pyc +0 -0
- proxy_auth_extension/__pycache__/proxy_auth_extension.cpython-310.pyc +0 -0
- proxy_auth_extension/background.js +28 -0
- proxy_auth_extension/manifest.json +18 -0
- proxy_auth_extension/proxy_auth_extension.py +63 -0
- requirements.txt +12 -0
- screen.py +35 -0
- srt.py +5 -0
- upload.py +706 -0
- utils.py +37 -0
app.py
CHANGED
|
@@ -1,35 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
from io import BytesIO
|
| 6 |
|
| 7 |
-
|
| 8 |
-
options = webdriver.ChromeOptions()
|
| 9 |
-
options.add_argument('--headless')
|
| 10 |
-
options.add_argument('--no-sandbox')
|
| 11 |
-
options.add_argument('--disable-dev-shm-usage')
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
try:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pixabay.core
|
| 2 |
+
import glob
|
| 3 |
+
from moviepy.editor import VideoFileClip, ImageClip, concatenate_videoclips, AudioFileClip,CompositeVideoClip, TextClip
|
| 4 |
+
import requests
|
| 5 |
+
import random
|
| 6 |
+
import moviepy.editor as mp
|
| 7 |
import gradio as gr
|
| 8 |
+
import time
|
| 9 |
+
import re
|
| 10 |
+
import os
|
|
|
|
| 11 |
|
| 12 |
+
from googletrans import Translator
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
import ffmpeg
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import openai
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
openai.api_key = os.environ["API_KEY"] # 首先要有apikey
|
| 21 |
+
|
| 22 |
+
#openai.api_key = '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'
|
| 23 |
+
|
| 24 |
+
messages = [{"role": "system", "content": "eres una mano derecha"}]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def chatgpt(content, temperature=0.8):
|
| 28 |
+
global messages
|
| 29 |
+
print(f'ask:{content}')
|
| 30 |
+
messages.append({"role": "user", "content": content})
|
| 31 |
+
time.sleep(1)
|
| 32 |
try:
|
| 33 |
+
response = openai.ChatCompletion.create(
|
| 34 |
+
model="gpt-3.5-turbo-0301", # 或者 gpt-3.5-turbo-0301
|
| 35 |
+
messages=messages,
|
| 36 |
+
temperature=temperature,
|
| 37 |
+
max_tokens=1000,
|
| 38 |
+
top_p=1,
|
| 39 |
+
frequency_penalty=0,
|
| 40 |
+
presence_penalty=0,
|
| 41 |
+
)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
messages = [{"role": "system", "content": "eres una mano derecha"}]
|
| 44 |
+
return 'Este mensaje interactivo ha alcanzado el límite superior, el token se ha borrado, vuelva a ingresar'
|
| 45 |
+
messages.append(json.loads(str(response.choices[0].message)))
|
| 46 |
+
print(f'answer:{response.choices[0].message.content}')
|
| 47 |
+
resulado = response.choices[0].message.content
|
| 48 |
+
resulado = resulado.replace('.','')
|
| 49 |
+
return resulado
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
pixabayapi = ''
|
| 54 |
+
def generaraudio2(texto):
|
| 55 |
+
nombre = s=re.sub('[^A-Za-z0-9]+', '',texto)
|
| 56 |
+
nombre = nombre.strip()
|
| 57 |
+
archivo = 'es_'+nombre+'.wav'
|
| 58 |
+
print(archivo)
|
| 59 |
+
existe = glob.glob(archivo)
|
| 60 |
+
texto = texto.lower()
|
| 61 |
+
texto = texto.replace('html5','hache te eme ele cinco')
|
| 62 |
+
texto = texto.replace('html','hache te eme ele')
|
| 63 |
+
texto = texto.replace('pdf','pe de eefe')
|
| 64 |
+
texto = texto.replace('bit.ly','bit punto li!, eslach,')
|
| 65 |
+
texto = texto.replace('.',',')
|
| 66 |
+
texto = texto.replace('1','uno')
|
| 67 |
+
texto = texto.replace('2','dos')
|
| 68 |
+
texto = texto.replace('3','tres')
|
| 69 |
+
texto = texto.replace('4','cuatro')
|
| 70 |
+
texto = texto.replace('5','cinco')
|
| 71 |
+
texto = texto.replace('6','seis')
|
| 72 |
+
texto = texto.replace('7','siete')
|
| 73 |
+
texto = texto.replace('8','ocho')
|
| 74 |
+
texto = texto.replace('9','nueve')
|
| 75 |
+
if not existe:
|
| 76 |
+
response = requests.post("https://iricardoxd-texto-a-voz-aws.hf.space/api/predict", json={
|
| 77 |
+
"data": [
|
| 78 |
+
texto
|
| 79 |
+
]},headers={'Content-Type':'application/json','Authorization': 'Bearer {}'.format('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')}).json()
|
| 80 |
+
data = response["data"]
|
| 81 |
+
print(response['data'][0]['name'])
|
| 82 |
+
time.sleep(5)
|
| 83 |
+
response = requests.get("https://iricardoxd-texto-a-voz-aws.hf.space/file="+response['data'][0]['name'],headers={'Content-Type':'application/json','Authorization': 'Bearer {}'.format('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')})
|
| 84 |
+
if response.status_code == 200:
|
| 85 |
+
open(archivo, 'wb').write(response.content)
|
| 86 |
+
print('downloaded')
|
| 87 |
+
else:
|
| 88 |
+
print(response.status_code)
|
| 89 |
+
return archivo
|
| 90 |
+
import requests
|
| 91 |
+
import shutil
|
| 92 |
+
def buscar(palabra,category):
|
| 93 |
+
xd = palabra.split()
|
| 94 |
+
xdfile = '_'.join(xd)
|
| 95 |
+
xdfile = xdfile.replace('/','').replace('"','').replace('.','').replace('!','').replace('@','').replace('\\','').replace('\n','').replace('\rn','').replace(':','').replace(',','')
|
| 96 |
+
archivo = category+'_'+xdfile+".mp4"
|
| 97 |
+
#existe = glob.glob(archivo)
|
| 98 |
+
if True:#not existe:
|
| 99 |
+
# Establece tu clave de API de Pexels
|
| 100 |
+
PEXELS_API_KEY = '563492ad6f917000010000011b2f873942744900a033f732a56ae4b7'
|
| 101 |
+
translator = Translator()
|
| 102 |
+
search_term = translator.translate(palabra, dest='en').text#blob.translate(to='en')
|
| 103 |
+
print(search_term)
|
| 104 |
+
# Crea el URL de la API
|
| 105 |
+
api_url = f'https://api.pexels.com/videos/search?query={search_term}&orientation=portrait&per_page=3&locale=en-US'
|
| 106 |
+
print(api_url)
|
| 107 |
+
# Establece los encabezados de la solicitud
|
| 108 |
+
headers = {'Authorization': PEXELS_API_KEY}
|
| 109 |
+
|
| 110 |
+
# Realiza la solicitud a la API
|
| 111 |
+
response = requests.get(api_url, headers=headers)
|
| 112 |
+
|
| 113 |
+
# Verifica si la solicitud fue exitosa
|
| 114 |
+
if response.status_code == 200:
|
| 115 |
+
# Obtiene los resultados de la búsqueda
|
| 116 |
+
results = response.json()['videos']
|
| 117 |
+
#print(results)
|
| 118 |
+
# Itera sobre los resultados
|
| 119 |
+
if len(results)>0:
|
| 120 |
+
ancho = 1
|
| 121 |
+
ancho = ancho - 1
|
| 122 |
+
numero = random.randint(0,ancho)
|
| 123 |
+
print('el numero es ',numero)
|
| 124 |
+
# Obtiene el URL del video
|
| 125 |
+
for videox in results[numero]['video_files']:
|
| 126 |
+
if videox['height']==1920:
|
| 127 |
+
video_url = videox['link']
|
| 128 |
+
else:
|
| 129 |
+
if videox['quality']=='hd':
|
| 130 |
+
video_url = videox['link']
|
| 131 |
+
else:
|
| 132 |
+
video_url = results[numero]['video_files'][0]['link']
|
| 133 |
+
print('el video es',video_url)
|
| 134 |
+
|
| 135 |
+
# Descarga el video
|
| 136 |
+
with requests.get(video_url, stream=True) as r:
|
| 137 |
+
with open(archivo, 'wb') as f:
|
| 138 |
+
shutil.copyfileobj(r.raw, f)
|
| 139 |
+
|
| 140 |
+
print(f'Descargado video {numero} de {len(results)}')
|
| 141 |
+
else:
|
| 142 |
+
api_url2 = f'https://api.pexels.com/v1/search?query={search_term}&per_page=1&orientation=portrait&locale=en-US'
|
| 143 |
+
print(api_url2)
|
| 144 |
+
response2 = requests.get(api_url2, headers=headers)
|
| 145 |
+
if response2.status_code == 200:
|
| 146 |
+
# Obtiene los resultados de la búsqueda
|
| 147 |
+
results2 = response2.json()['photos']
|
| 148 |
+
# Itera sobre los resultados
|
| 149 |
+
if len(results2)>0:
|
| 150 |
+
ancho = 1
|
| 151 |
+
ancho = ancho - 1
|
| 152 |
+
numero = random.randint(0,ancho)
|
| 153 |
+
print('el numero es ',numero)
|
| 154 |
+
# Obtiene el URL del video
|
| 155 |
+
video_url = results2[numero]['src']['portrait']
|
| 156 |
+
imagen = results2[numero]['src']['original']
|
| 157 |
+
filex = imagen.split('/')
|
| 158 |
+
print(' imagen es',video_url)
|
| 159 |
+
x=len(filex)-1
|
| 160 |
+
archivo=filex[x]
|
| 161 |
+
print(' imagen es x ',archivo)
|
| 162 |
+
# Descarga el video
|
| 163 |
+
with requests.get(video_url, stream=True) as r:
|
| 164 |
+
with open(archivo, 'wb') as f:
|
| 165 |
+
shutil.copyfileobj(r.raw, f)
|
| 166 |
+
|
| 167 |
+
print(f'Descargado imagen {numero} de {len(results)}')
|
| 168 |
+
else:
|
| 169 |
+
archivo='doctor.mp4'
|
| 170 |
+
else:
|
| 171 |
+
print('Error al realizar la solicitud a la API')
|
| 172 |
+
return archivo
|
| 173 |
+
def buscarsustantivo(frase):
|
| 174 |
+
if frase !='':
|
| 175 |
+
prompt ='Respond only with one word of videos availables in pixels.com that best expresses the English phrase: '+frase
|
| 176 |
+
return chatgpt(prompt)
|
| 177 |
+
|
| 178 |
+
def combinar(frase, api_pixabay):
|
| 179 |
+
global pixabayapi
|
| 180 |
+
pixabayapi = '38938115-4ffafdfccf94d14e4d35899f5'
|
| 181 |
+
category="travel"
|
| 182 |
+
clips = []
|
| 183 |
+
palabras = frase.split(' ')
|
| 184 |
+
palabras_frases = frase.split('.')
|
| 185 |
+
frases = []
|
| 186 |
+
i = 0
|
| 187 |
+
linea = ''
|
| 188 |
+
for palabra in palabras:
|
| 189 |
+
i = i + 1
|
| 190 |
+
linea = linea + ' ' + palabra
|
| 191 |
+
escena_palabras = random.randint(1, 5)
|
| 192 |
+
if i>escena_palabras or '.' in linea or '?' in linea or '!' in linea:
|
| 193 |
+
i=0
|
| 194 |
+
frases.append(linea)
|
| 195 |
+
linea = ''
|
| 196 |
+
if linea !='':
|
| 197 |
+
frases.append(linea)
|
| 198 |
+
duracionTotal = 0
|
| 199 |
+
for lineaf in frases:
|
| 200 |
+
tema = buscarsustantivo(lineaf)
|
| 201 |
+
palabras_de_tema = tema.split(' ')
|
| 202 |
+
if len(palabras_de_tema)>1:
|
| 203 |
+
tema = palabras_de_tema[0]
|
| 204 |
+
#for tema in temas:
|
| 205 |
+
if len(tema)>0:
|
| 206 |
+
palabraclave = tema
|
| 207 |
+
else:
|
| 208 |
+
palabraclave = frases[0]
|
| 209 |
+
print(palabraclave)
|
| 210 |
+
resultado = buscar(palabraclave,category)
|
| 211 |
+
if(resultado=='lv_0_20230901171552.mp4'):
|
| 212 |
+
tema = buscarsustantivo(lineaf)
|
| 213 |
+
palabras_de_tema = tema.split(' ')
|
| 214 |
+
if len(palabras_de_tema)>1:
|
| 215 |
+
tema = palabras_de_tema[0]
|
| 216 |
+
#for tema in temas:
|
| 217 |
+
if len(tema)>0:
|
| 218 |
+
palabraclave = tema
|
| 219 |
+
else:
|
| 220 |
+
palabraclave = frases[0]
|
| 221 |
+
print(palabraclave)
|
| 222 |
+
resultado = buscar(palabraclave,category)
|
| 223 |
+
if resultado!=None:
|
| 224 |
+
velocidadhabla = 15#catacteres por segundos 18 teresa, 22 huggie
|
| 225 |
+
print(len(lineaf))
|
| 226 |
+
duracion = len(lineaf)/velocidadhabla
|
| 227 |
+
print(resultado)
|
| 228 |
+
try:
|
| 229 |
+
audiofile = generaraudio2(lineaf)
|
| 230 |
+
clip1 = VideoFileClip(generate_subtitled_video(resultado,lineaf))
|
| 231 |
+
audio = AudioFileClip(audiofile)
|
| 232 |
+
#duracion = audio.duration
|
| 233 |
+
duracionTotal = duracionTotal + duracion
|
| 234 |
+
subclip = clip1.subclip(0, duracion)
|
| 235 |
+
resized_clip = subclip.resize((1080, 1920))
|
| 236 |
+
video = resized_clip.resize(height=1920)
|
| 237 |
+
final_clip = video#final_clip = video.crop(x1=1166.6,y1=0,x2=2246.6,y2=1920)
|
| 238 |
+
final_clip.audio = audio
|
| 239 |
+
clips.append(final_clip)
|
| 240 |
+
except Exception as e:
|
| 241 |
+
print(f'Ocurrió un error: {e}')
|
| 242 |
+
clip = ImageClip(resultado)
|
| 243 |
+
# Establece la duración del clip en segundos
|
| 244 |
+
clip = clip.set_duration(5)
|
| 245 |
+
# Guarda el clip como un archivo de video
|
| 246 |
+
clip.write_videofile('videoxd.mp4', fps=24)
|
| 247 |
+
audiofile = generaraudio2(lineaf)
|
| 248 |
+
clip1 = VideoFileClip(generate_subtitled_video('videoxd.mp4',lineaf))#generate_subtitled_video('videoxd.mp4',audiofile,lineaf))
|
| 249 |
+
audio = AudioFileClip(audiofile)
|
| 250 |
+
#duracion = audio.duration
|
| 251 |
+
duracionTotal = duracionTotal + duracion
|
| 252 |
+
subclip = clip1.subclip(0, duracion)
|
| 253 |
+
resized_clip = subclip.resize((1080, 1920))
|
| 254 |
+
video = resized_clip.resize(height=1920)
|
| 255 |
+
final_clip = video#final_clip = video.crop(x1=1166.6,y1=0,x2=2246.6,y2=1920)
|
| 256 |
+
#final_clip.audio = audio
|
| 257 |
+
clips.append(final_clip)
|
| 258 |
+
filename=palabras_frases[0]+'.mp4'
|
| 259 |
+
if len(clips)>0:
|
| 260 |
+
audiofilefinal = generaraudio2(frase)
|
| 261 |
+
final_clip = concatenate_videoclips(clips)
|
| 262 |
+
final_clip.audio = AudioFileClip(audiofilefinal)
|
| 263 |
+
final_clip.write_videofile(filename)
|
| 264 |
+
print('listo video')
|
| 265 |
+
final = filename
|
| 266 |
+
subir(filename,palabras_frases[0])
|
| 267 |
+
html= '<a href="file/'+final+'" download>Descargar video</a><a href="file/final.mp4" download>Descargar video</a>'
|
| 268 |
+
return (final,html)
|
| 269 |
+
def generate_subtitled_video(video, transcript):
|
| 270 |
+
input = ffmpeg.input(video)
|
| 271 |
+
output = ffmpeg.output(input, 'srt'+video, vf="drawtext=text='"+transcript+"':fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=((h-text_h-line_h)/9)*10")
|
| 272 |
+
output.run(quiet=True, overwrite_output=True)
|
| 273 |
+
video_with_subs = 'srt'+video
|
| 274 |
+
print(video_with_subs)
|
| 275 |
+
return video_with_subs
|
| 276 |
+
def subir(video,descripcion):
|
| 277 |
+
from upload import upload_videos
|
| 278 |
+
from auth import AuthBackend
|
| 279 |
+
|
| 280 |
+
videos = [
|
| 281 |
+
{
|
| 282 |
+
'video': video,
|
| 283 |
+
'description': descripcion
|
| 284 |
+
}
|
| 285 |
+
]
|
| 286 |
+
|
| 287 |
+
auth = AuthBackend(cookies='cookies.txt')
|
| 288 |
+
failed_videos = upload_videos(videos=videos, auth=auth)
|
| 289 |
+
|
| 290 |
+
for video in failed_videos: # each input video object which failed
|
| 291 |
+
print(f'{video["video"]} with description "{video["description"]}" failed')
|
| 292 |
+
demo = gr.Interface(fn=combinar, inputs=["text","text"], outputs=["video","html"])
|
| 293 |
+
demo.launch()
|
auth.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Handles authentication for TikTokUploader"""
|
| 2 |
+
from http import cookiejar
|
| 3 |
+
from time import time, sleep
|
| 4 |
+
|
| 5 |
+
from selenium.webdriver.common.by import By
|
| 6 |
+
|
| 7 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
| 8 |
+
from selenium.webdriver.support import expected_conditions as EC
|
| 9 |
+
|
| 10 |
+
from tiktok_uploader import config, logger
|
| 11 |
+
from tiktok_uploader.browsers import get_browser
|
| 12 |
+
from tiktok_uploader.utils import green
|
| 13 |
+
|
| 14 |
+
class AuthBackend:
|
| 15 |
+
"""
|
| 16 |
+
Handles authentication for TikTokUploader
|
| 17 |
+
"""
|
| 18 |
+
username: str
|
| 19 |
+
password: str
|
| 20 |
+
cookies: list
|
| 21 |
+
|
| 22 |
+
def __init__(self, username: str = '', password: str = '',
|
| 23 |
+
cookies_list: list = None, cookies=None, cookies_str=None, sessionid: str = None):
|
| 24 |
+
"""
|
| 25 |
+
Creates the authenticaiton backend
|
| 26 |
+
|
| 27 |
+
Keyword arguments:
|
| 28 |
+
- username -> the accounts's username or email
|
| 29 |
+
- password -> the account's password
|
| 30 |
+
|
| 31 |
+
- cookies -> a list of cookie dictionaries of cookies which is Selenium-compatable
|
| 32 |
+
"""
|
| 33 |
+
if (username and not password) or (password and not username):
|
| 34 |
+
raise InsufficientAuth()
|
| 35 |
+
|
| 36 |
+
self.cookies = self.get_cookies(path=cookies) if cookies else []
|
| 37 |
+
self.cookies += self.get_cookies(cookies_str=cookies_str) if cookies_str else []
|
| 38 |
+
self.cookies += cookies_list if cookies_list else []
|
| 39 |
+
self.cookies += [{'name': 'sessionid', 'value': sessionid}] if sessionid else []
|
| 40 |
+
|
| 41 |
+
if not (self.cookies or (username and password)):
|
| 42 |
+
raise InsufficientAuth()
|
| 43 |
+
|
| 44 |
+
self.username = username
|
| 45 |
+
self.password = password
|
| 46 |
+
|
| 47 |
+
if cookies:
|
| 48 |
+
logger.debug(green("Authenticating browser with cookies"))
|
| 49 |
+
elif username and password:
|
| 50 |
+
logger.debug(green("Authenticating browser with username and password"))
|
| 51 |
+
elif sessionid:
|
| 52 |
+
logger.debug(green("Authenticating browser with sessionid"))
|
| 53 |
+
elif cookies_list:
|
| 54 |
+
logger.debug(green("Authenticating browser with cookies_list"))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def authenticate_agent(self, driver):
|
| 58 |
+
"""
|
| 59 |
+
Authenticates the agent using the browser backend
|
| 60 |
+
"""
|
| 61 |
+
# tries to use cookies
|
| 62 |
+
if not self.cookies and self.username and self.password:
|
| 63 |
+
self.cookies = login(driver, username=self.username, password=self.password)
|
| 64 |
+
|
| 65 |
+
logger.debug(green("Authenticating browser with cookies"))
|
| 66 |
+
|
| 67 |
+
driver.get(config['paths']['main'])
|
| 68 |
+
|
| 69 |
+
WebDriverWait(driver, config['explicit_wait']).until(EC.title_contains("TikTok"))
|
| 70 |
+
|
| 71 |
+
for cookie in self.cookies:
|
| 72 |
+
try:
|
| 73 |
+
driver.add_cookie(cookie)
|
| 74 |
+
except Exception as _:
|
| 75 |
+
logger.error('Failed to add cookie %s', cookie)
|
| 76 |
+
|
| 77 |
+
return driver
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def get_cookies(self, path: str = None, cookies_str: str = None) -> dict:
|
| 81 |
+
"""
|
| 82 |
+
Gets cookies from the passed file using the netscape standard
|
| 83 |
+
"""
|
| 84 |
+
if path:
|
| 85 |
+
with open(path, "r", encoding="utf-8") as file:
|
| 86 |
+
lines = file.read().split("\n")
|
| 87 |
+
else:
|
| 88 |
+
lines = cookies_str.split("\n")
|
| 89 |
+
|
| 90 |
+
return_cookies = []
|
| 91 |
+
for line in lines:
|
| 92 |
+
split = line.split('\t')
|
| 93 |
+
if len(split) < 6:
|
| 94 |
+
continue
|
| 95 |
+
|
| 96 |
+
split = [x.strip() for x in split]
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
split[4] = int(split[4])
|
| 100 |
+
except ValueError:
|
| 101 |
+
split[4] = None
|
| 102 |
+
|
| 103 |
+
return_cookies.append({
|
| 104 |
+
'name': split[5],
|
| 105 |
+
'value': split[6],
|
| 106 |
+
'domain': split[0],
|
| 107 |
+
'path': split[2],
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
if split[4]:
|
| 111 |
+
return_cookies[-1]['expiry'] = split[4]
|
| 112 |
+
return return_cookies
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def login_accounts(driver=None, accounts=[(None, None)], *args, **kwargs) -> list:
|
| 116 |
+
"""
|
| 117 |
+
Authenticates the accounts using the browser backend and saves the required credentials
|
| 118 |
+
|
| 119 |
+
Keyword arguments:
|
| 120 |
+
- driver -> the webdriver to use
|
| 121 |
+
- accounts -> a list of tuples of the form (username, password)
|
| 122 |
+
"""
|
| 123 |
+
driver = driver or get_browser(headless=True, *args, **kwargs)
|
| 124 |
+
|
| 125 |
+
cookies = {}
|
| 126 |
+
for account in accounts:
|
| 127 |
+
username, password = get_username_and_password(account)
|
| 128 |
+
|
| 129 |
+
cookies[username] = login(driver, username, password)
|
| 130 |
+
|
| 131 |
+
return cookies
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def login(driver, username: str, password: str):
|
| 135 |
+
"""
|
| 136 |
+
Logs in the user using the email and password
|
| 137 |
+
"""
|
| 138 |
+
assert username and password, "Username and password are required"
|
| 139 |
+
|
| 140 |
+
# checks if the browser is on TikTok
|
| 141 |
+
if not config['paths']['main'] in driver.current_url:
|
| 142 |
+
driver.get(config['paths']['main'])
|
| 143 |
+
|
| 144 |
+
# checks if the user is already logged in
|
| 145 |
+
if driver.get_cookie(config['selectors']['login']['cookie_of_interest']):
|
| 146 |
+
# clears the existing cookies
|
| 147 |
+
driver.delete_all_cookies()
|
| 148 |
+
|
| 149 |
+
# goes to the login site
|
| 150 |
+
driver.get(config['paths']['login'])
|
| 151 |
+
|
| 152 |
+
# selects and fills the login and the password
|
| 153 |
+
username_field = WebDriverWait(driver, config['explicit_wait']).until(
|
| 154 |
+
EC.presence_of_element_located((By.XPATH, config['selectors']['login']['username_field']))
|
| 155 |
+
)
|
| 156 |
+
username_field.clear()
|
| 157 |
+
username_field.send_keys(username)
|
| 158 |
+
|
| 159 |
+
password_field = driver.find_element(By.XPATH, config['selectors']['login']['password_field'])
|
| 160 |
+
password_field.clear()
|
| 161 |
+
password_field.send_keys(password)
|
| 162 |
+
|
| 163 |
+
# submits the form
|
| 164 |
+
submit = driver.find_element(By.XPATH, config['selectors']['login']['login_button'])
|
| 165 |
+
submit.click()
|
| 166 |
+
|
| 167 |
+
print(f'Complete the captcha for {username}')
|
| 168 |
+
|
| 169 |
+
# Wait until the session id cookie is set
|
| 170 |
+
start_time = time()
|
| 171 |
+
while not driver.get_cookie(config['selectors']['login']['cookie_of_interest']):
|
| 172 |
+
sleep(0.5)
|
| 173 |
+
if time() - start_time > config['explicit_wait']:
|
| 174 |
+
raise InsufficientAuth() # TODO: Make this something more real
|
| 175 |
+
|
| 176 |
+
# wait until the url changes
|
| 177 |
+
WebDriverWait(driver, config['explicit_wait']).until(EC.url_changes(config['paths']['login']))
|
| 178 |
+
|
| 179 |
+
return driver.get_cookies()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def get_username_and_password(login_info: tuple or dict):
|
| 183 |
+
"""
|
| 184 |
+
Parses the input into a username and password
|
| 185 |
+
"""
|
| 186 |
+
if not isinstance(login_info, dict):
|
| 187 |
+
return login_info[0], login_info[1]
|
| 188 |
+
|
| 189 |
+
# checks if they used email or username
|
| 190 |
+
if 'email' in login_info:
|
| 191 |
+
return login_info['email'], login_info['password']
|
| 192 |
+
elif 'username' in login_info:
|
| 193 |
+
return login_info['username'], login_info['password']
|
| 194 |
+
|
| 195 |
+
raise InsufficientAuth()
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def save_cookies(path, cookies: list):
|
| 199 |
+
"""
|
| 200 |
+
Saves the cookies to a netscape file
|
| 201 |
+
"""
|
| 202 |
+
# saves the cookies to a file
|
| 203 |
+
cookie_jar = cookiejar.MozillaCookieJar(path)
|
| 204 |
+
cookie_jar.load()
|
| 205 |
+
|
| 206 |
+
for cookie in cookies:
|
| 207 |
+
cookie_jar.set_cookie(cookie)
|
| 208 |
+
|
| 209 |
+
cookie_jar.save()
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class InsufficientAuth(Exception):
|
| 213 |
+
"""
|
| 214 |
+
Insufficient authentication:
|
| 215 |
+
|
| 216 |
+
> TikTok uses cookies to keep track of the user's authentication or session.
|
| 217 |
+
|
| 218 |
+
Either:
|
| 219 |
+
- Use a cookies file passed as the `cookies` argument
|
| 220 |
+
- easily obtained using https://github.com/kairi003/Get-cookies.txt-LOCALLY
|
| 221 |
+
- Use a cookies list passed as the `cookies_list` argument
|
| 222 |
+
- can be obtained from your browser's developer tools under storage -> cookies
|
| 223 |
+
- only the `sessionid` cookie is required
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
def __init__(self, message=None):
|
| 227 |
+
super().__init__(message or self.__doc__)
|
config.toml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TikTok Uploader Default Configuation File
|
| 2 |
+
|
| 3 |
+
headless = true
|
| 4 |
+
quit_on_end = true
|
| 5 |
+
|
| 6 |
+
# Messing around with inputs
|
| 7 |
+
valid_path_names = ["path", "filename", "video", "video_path"]
|
| 8 |
+
valid_descriptions = ["description", "desc", "caption"]
|
| 9 |
+
|
| 10 |
+
# Selenium Webdriver Waits
|
| 11 |
+
implicit_wait = 5 # seconds
|
| 12 |
+
explicit_wait = 60 # seconds
|
| 13 |
+
|
| 14 |
+
supported_file_types = ["mp4", "mov", "avi", "wmv", "flv", "webm", "mkv", "m4v", "3gp", "3g2", "gif"]
|
| 15 |
+
|
| 16 |
+
max_description_length = 150 # characters
|
| 17 |
+
|
| 18 |
+
[paths]
|
| 19 |
+
main = "https://www.tiktok.com/"
|
| 20 |
+
login = "https://www.tiktok.com/login/phone-or-email/email"
|
| 21 |
+
upload = "https://www.tiktok.com/creator-center/upload?from=upload"
|
| 22 |
+
|
| 23 |
+
[disguising]
|
| 24 |
+
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
|
| 25 |
+
|
| 26 |
+
[selectors] # Selenium XPATH selectors
|
| 27 |
+
|
| 28 |
+
[selectors.login]
|
| 29 |
+
username_field = "//input[@name=\"username\"]"
|
| 30 |
+
password_field = "//input[@type=\"password\"]"
|
| 31 |
+
login_button = "//button[@type=\"submit\"]"
|
| 32 |
+
|
| 33 |
+
alert_user_if_failed = true # sends an alert and waits instead of failing
|
| 34 |
+
|
| 35 |
+
cookie_of_interest = "sessionid" # cookie to check if login was successful
|
| 36 |
+
|
| 37 |
+
[selectors.upload]
|
| 38 |
+
iframe = "//iframe"
|
| 39 |
+
|
| 40 |
+
upload_video = "//input[@type='file']"
|
| 41 |
+
upload_in_progress = "//*[.='Cancelar']"
|
| 42 |
+
upload_confirmation = "//video"
|
| 43 |
+
process_confirmation = "//img[@draggable='false']"
|
| 44 |
+
|
| 45 |
+
description = "//div[@contenteditable='true']"
|
| 46 |
+
|
| 47 |
+
visibility = "//div[@class='tiktok-select-selector']"
|
| 48 |
+
options = ["Public", "Friends", "Private"]
|
| 49 |
+
|
| 50 |
+
hashtags = "//div[@class='mentionSuggestions']//*[contains(text(), '{}')]"
|
| 51 |
+
mentions = "//div[contains(concat(' ', normalize-space(@class), ' '), 'user-id') and .='{}']/.."
|
| 52 |
+
|
| 53 |
+
mention_box = "//input[contains(concat(' ', normalize-space(@class), ' '), 'search-friends')]"
|
| 54 |
+
|
| 55 |
+
comment = "//label[.='Comment']/following-sibling::div/input"
|
| 56 |
+
duet = "//label[.='Duet']/following-sibling::div/input"
|
| 57 |
+
stitch = "//label[.='Stitch']/following-sibling::div/input"
|
| 58 |
+
|
| 59 |
+
post = "//div[contains(@class, 'btn-post')]"
|
| 60 |
+
post_confirmation = "//div[.='Your videos are being uploaded to TikTok!']"
|
| 61 |
+
|
| 62 |
+
[selectors.schedule]
|
| 63 |
+
switch = "//*[@id='tux-3']"
|
| 64 |
+
|
| 65 |
+
date_picker = "//div[contains(@class, 'date-picker-input')]"
|
| 66 |
+
calendar = "//div[contains(@class, 'calendar-wrapper')]"
|
| 67 |
+
calendar_month = "//span[contains(@class, 'month-title')]"
|
| 68 |
+
calendar_valid_days = "//div[@class='jsx-4172176419 days-wrapper']//span[contains(@class, 'day') and contains(@class, 'valid')]"
|
| 69 |
+
calendar_arrows = "//span[contains(@class, 'arrow')]" # first last, second next
|
| 70 |
+
|
| 71 |
+
time_picker = "//div[contains(@class, 'time-picker-input')]"
|
| 72 |
+
time_picker_text = "//div[contains(@class, 'time-picker-input')]/*[1]"
|
| 73 |
+
time_picker_container = "//div[@class='tiktok-timepicker-time-picker-container']"
|
| 74 |
+
timepicker_hours = "//span[contains(@class, 'tiktok-timepicker-left')]"
|
| 75 |
+
timepicker_minutes = "//span[contains(@class, 'tiktok-timepicker-right')]"
|
cookies.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Netscape HTTP Cookie File
|
| 2 |
+
# http://curl.haxx.se/rfc/cookie_spec.html
|
| 3 |
+
# This is a generated file! Do not edit.
|
| 4 |
+
|
| 5 |
+
.www.tiktok.com TRUE / TRUE 1696204457 tiktok_webapp_theme light
|
| 6 |
+
.tiktok.com TRUE / TRUE 1700680950 passport_csrf_token 2fcbe4468644121124e160b2bd58f363
|
| 7 |
+
.tiktok.com TRUE / FALSE 1700680950 passport_csrf_token_default 2fcbe4468644121124e160b2bd58f363
|
| 8 |
+
.tiktok.com TRUE / TRUE 1711151649 tt_chain_token Rt2x5VfQLupBGHX2zAaeKQ==
|
| 9 |
+
.www.tiktok.com TRUE / FALSE 1696204455 __tea_cache_tokens_1988 {%22_type_%22:%22default%22%2C%22user_unique_id%22:%227282105198122092075%22%2C%22timestamp%22:1695497380126}
|
| 10 |
+
.tiktok.com TRUE / TRUE 1700681741 multi_sids 7282105716742734890%3A747d8454c7769596ecaf4ae3d1579662
|
| 11 |
+
.tiktok.com TRUE / TRUE 1700681741 cmpl_token AgQQAPNSF-RO0rUx0janeB0T_26lK8CI_6nZYMy3Aw
|
| 12 |
+
.tiktok.com TRUE / TRUE 1711049741 sid_guard 747d8454c7769596ecaf4ae3d1579662%7C1695497740%7C15552000%7CThu%2C+21-Mar-2024+19%3A35%3A40+GMT
|
| 13 |
+
.tiktok.com TRUE / TRUE 1711049741 uid_tt 16c13984b11d11e7bcbf81c40c4fc99d4589d115466d2c20f7579350a6ffedd5
|
| 14 |
+
.tiktok.com TRUE / TRUE 1711049741 uid_tt_ss 16c13984b11d11e7bcbf81c40c4fc99d4589d115466d2c20f7579350a6ffedd5
|
| 15 |
+
.tiktok.com TRUE / TRUE 1711049741 sid_tt 747d8454c7769596ecaf4ae3d1579662
|
| 16 |
+
.tiktok.com TRUE / TRUE 1711049741 sessionid 747d8454c7769596ecaf4ae3d1579662
|
| 17 |
+
.tiktok.com TRUE / TRUE 1711049741 sessionid_ss 747d8454c7769596ecaf4ae3d1579662
|
| 18 |
+
.tiktok.com TRUE / TRUE 1711049741 sid_ucp_v1 1.0.0-KDcyZWZmZjFjMWViNTgwMWMzZDQ3NjRhNTU1NzRjMGIxY2QyM2JkMTUKIAiqiKv6kpLPh2UQjPy8qAYYswsgDDCf-byoBjgBQOsHEAQaB3VzZWFzdDUiIDc0N2Q4NDU0Yzc3Njk1OTZlY2FmNGFlM2QxNTc5NjYy
|
| 19 |
+
.tiktok.com TRUE / TRUE 1711049741 ssid_ucp_v1 1.0.0-KDcyZWZmZjFjMWViNTgwMWMzZDQ3NjRhNTU1NzRjMGIxY2QyM2JkMTUKIAiqiKv6kpLPh2UQjPy8qAYYswsgDDCf-byoBjgBQOsHEAQaB3VzZWFzdDUiIDc0N2Q4NDU0Yzc3Njk1OTZlY2FmNGFlM2QxNTc5NjYy
|
| 20 |
+
.tiktok.com TRUE / FALSE 1711049740 store-idc useast5
|
| 21 |
+
.tiktok.com TRUE / FALSE 1711049740 store-country-code us
|
| 22 |
+
.tiktok.com TRUE / FALSE 1711049740 store-country-code-src uid
|
| 23 |
+
.tiktok.com TRUE / FALSE 1711049740 tt-target-idc useast5
|
| 24 |
+
.tiktok.com TRUE / FALSE 1711049754 tt-target-idc-sign 3G_umYol_pR5-9ql6JlmS_vAG-oxfye8T1PW1DwPhJstsWwuBM2naJyAcfB0zdi3iVioRSpKo9S7EMPiSOtqiUbUo8-iB_LMGkSkCCB8NjPZxq3XvWw4TuTHJrAQE-KKAWVvLMPRpGHhmPYaiaVS3V6rRCKbGyZXvw26F9jfVNOPTrobntczrQMSR7wnwLPfyIKmKLsqZ3FZUS8u8ZakrgZhVK1c9P9wRHo0rdrWJ8p871cqBu9lVYoeVFL-eT8LaYN3EjWJK_S3ySqXuIJTsABbn9RokzBI4kEIx7Rysds1bBidJPPC6N29PkJbwSnaniSy4Q1kRu9oQNl-d80LsqMcRaP6gj19zVkmwoTBQ7bXaIJoo1-nq2WGA7EhlTroDhbxsS_mnV1e12UASiigajxfccDY8k3bU4qej4FQj0_73OEQVpRayxr9jK34w9CgGozzxVRFfyIAtAnTkpwZmTzqxrOhYn5JW3mI4HaK-xtAeI88l34FC6uXMw2Yg0aH
|
| 25 |
+
.tiktok.com TRUE / TRUE 0 tt_csrf_token YVpr7bGu-pagI4x0nJWqcHju8QT9akkzF-K4
|
| 26 |
+
.www.tiktok.com TRUE / FALSE 0 passport_fe_beating_status true
|
| 27 |
+
.tiktok.com TRUE / TRUE 1711151664 ttwid 1%7CBffw97Kt4MoFtptEg0otV7ohXh9iyAh9ShYIEX2fUug%7C1695599664%7Cf95f7655258ee6bd0a66af03e4bb410e33f3fc54711a8e9e03f1f55507ae47a8
|
| 28 |
+
.tiktok.com TRUE / FALSE 1711151665 odin_tt 9c735b8191086bf88a56b57173f76d2b543edca8c95410eee89502a1f33861ebe6c9b08c2e01e2eb2e9c21cddc0eb82fadfdd2a9a8b738b16202e183f3ae0003ab015dc977f2a30bc485784adf36040a
|
| 29 |
+
www.tiktok.com FALSE / TRUE 0 csrf_session_id 91570471488ad14ddabf4f3f567cf8b4
|
| 30 |
+
.tiktok.com TRUE / TRUE 1696463793 msToken ULaJAPtAQlIhCv-aMXKoBMeeLfw0h_LYPD4nyD6nifbdb8c5rb-72ohf2PRIpn3DUwz8aD4HX-RTr6lB946CB2qqV3ulguPmFOJ7yWC9i4nHCA83Z3kaChw71JJr5Yif_iFbbIe-Cegg
|
| 31 |
+
www.tiktok.com FALSE / FALSE 1696204593 msToken ULaJAPtAQlIhCv-aMXKoBMeeLfw0h_LYPD4nyD6nifbdb8c5rb-72ohf2PRIpn3DUwz8aD4HX-RTr6lB946CB2qqV3ulguPmFOJ7yWC9i4nHCA83Z3kaChw71JJr5Yif_iFbbIe-Cegg
|
proxy_auth_extension/__init__.py
ADDED
|
File without changes
|
proxy_auth_extension/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (141 Bytes). View file
|
|
|
proxy_auth_extension/__pycache__/proxy_auth_extension.cpython-310.pyc
ADDED
|
Binary file (2.15 kB). View file
|
|
|
proxy_auth_extension/background.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
var config = {
|
| 2 |
+
mode: "fixed_servers",
|
| 3 |
+
rules: {
|
| 4 |
+
singleProxy: {
|
| 5 |
+
scheme: "http",
|
| 6 |
+
host: "{{ proxy_host }}",
|
| 7 |
+
port: parseInt("{{ proxy_port }}")
|
| 8 |
+
},
|
| 9 |
+
bypassList: ["localhost"]
|
| 10 |
+
}
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
|
| 14 |
+
|
| 15 |
+
function callbackFn(details) {
|
| 16 |
+
return {
|
| 17 |
+
authCredentials: {
|
| 18 |
+
username: "{{ proxy_user }}",
|
| 19 |
+
password: "{{ proxy_pass }}"
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
chrome.webRequest.onAuthRequired.addListener(
|
| 25 |
+
callbackFn,
|
| 26 |
+
{urls: ["<all_urls>"]},
|
| 27 |
+
['blocking']
|
| 28 |
+
);
|
proxy_auth_extension/manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"manifest_version": 2,
|
| 4 |
+
"name": "Chrome Proxy",
|
| 5 |
+
"permissions": [
|
| 6 |
+
"proxy",
|
| 7 |
+
"tabs",
|
| 8 |
+
"unlimitedStorage",
|
| 9 |
+
"storage",
|
| 10 |
+
"<all_urls>",
|
| 11 |
+
"webRequest",
|
| 12 |
+
"webRequestBlocking"
|
| 13 |
+
],
|
| 14 |
+
"background": {
|
| 15 |
+
"scripts": ["background.js"]
|
| 16 |
+
},
|
| 17 |
+
"minimum_chrome_version":"22.0.0"
|
| 18 |
+
}
|
proxy_auth_extension/proxy_auth_extension.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import zipfile
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from selenium.webdriver.common.by import By
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def replace_variables_in_js(js_content: str, variables_dict: dict):
|
| 8 |
+
for variable, value in variables_dict.items():
|
| 9 |
+
js_content = js_content.replace('{{ ' + variable + ' }}', value)
|
| 10 |
+
return js_content
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def generate_proxy_auth_extension(
|
| 14 |
+
proxy_host: str, proxy_port: str, proxy_user: str, proxy_pass: str,
|
| 15 |
+
extension_file: str):
|
| 16 |
+
"""Generate a Chrome extension that modify proxy settings based on desired host, port, username and password.
|
| 17 |
+
|
| 18 |
+
If you are using --headless in chromedriver, you must use --headless=new to support extensions in headless mode.
|
| 19 |
+
"""
|
| 20 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 21 |
+
manifest_json_path = os.path.join(current_dir, 'manifest.json')
|
| 22 |
+
background_js_path = os.path.join(current_dir, 'background.js')
|
| 23 |
+
with open(manifest_json_path, 'r', encoding='utf-8') as f:
|
| 24 |
+
manifest_json = f.read()
|
| 25 |
+
with open(background_js_path, 'r', encoding='utf-8') as f:
|
| 26 |
+
background_js = f.read()
|
| 27 |
+
|
| 28 |
+
variables_dict = {
|
| 29 |
+
'proxy_host': proxy_host,
|
| 30 |
+
'proxy_port': proxy_port,
|
| 31 |
+
'proxy_user': proxy_user,
|
| 32 |
+
'proxy_pass': proxy_pass
|
| 33 |
+
}
|
| 34 |
+
background_js = replace_variables_in_js(background_js, variables_dict)
|
| 35 |
+
|
| 36 |
+
with zipfile.ZipFile(extension_file, 'w') as zp:
|
| 37 |
+
zp.writestr('manifest.json', manifest_json)
|
| 38 |
+
zp.writestr('background.js', background_js)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def get_my_ip(driver):
|
| 42 |
+
origin_tab = driver.current_window_handle
|
| 43 |
+
driver.execute_script("window.open('', '_blank');")
|
| 44 |
+
driver.switch_to.window(driver.window_handles[-1])
|
| 45 |
+
|
| 46 |
+
driver.get('https://api.ipify.org/?format=json')
|
| 47 |
+
|
| 48 |
+
ip_row = driver.find_element(By.XPATH, '//body').text
|
| 49 |
+
ip = json.loads(ip_row)['ip']
|
| 50 |
+
|
| 51 |
+
driver.close()
|
| 52 |
+
driver.switch_to.window(origin_tab)
|
| 53 |
+
|
| 54 |
+
return ip
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def proxy_is_working(driver, host: str):
|
| 58 |
+
ip = get_my_ip(driver)
|
| 59 |
+
|
| 60 |
+
if ip == host:
|
| 61 |
+
return True
|
| 62 |
+
else:
|
| 63 |
+
return False
|
requirements.txt
CHANGED
|
@@ -1,3 +1,15 @@
|
|
| 1 |
selenium >=4.0.0, < 5.0.0
|
| 2 |
gradio>=3.40.1
|
| 3 |
Pillow>=8.3.1,<9.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
selenium >=4.0.0, < 5.0.0
|
| 2 |
gradio>=3.40.1
|
| 3 |
Pillow>=8.3.1,<9.0
|
| 4 |
+
googletrans==4.0.0rc1
|
| 5 |
+
moviepy==1.0.3
|
| 6 |
+
SpeechRecognition
|
| 7 |
+
glob2
|
| 8 |
+
spacy
|
| 9 |
+
voicerss_tts
|
| 10 |
+
requests
|
| 11 |
+
fakeyou
|
| 12 |
+
openai==0.27.0
|
| 13 |
+
pixabay==0.0.4
|
| 14 |
+
ffmpeg-python
|
| 15 |
+
tiktok_uploader
|
screen.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from selenium import webdriver
|
| 3 |
+
from selenium.common.exceptions import WebDriverException
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
|
| 7 |
+
def take_screenshot(url):
|
| 8 |
+
options = webdriver.ChromeOptions()
|
| 9 |
+
options.add_argument('--headless')
|
| 10 |
+
options.add_argument('--no-sandbox')
|
| 11 |
+
options.add_argument('--disable-dev-shm-usage')
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
wd = webdriver.Chrome(options=options)
|
| 15 |
+
wd.set_window_size(1080, 720) # Adjust the window size here
|
| 16 |
+
wd.get(url)
|
| 17 |
+
wd.implicitly_wait(10)
|
| 18 |
+
screenshot = wd.get_screenshot_as_png()
|
| 19 |
+
except WebDriverException as e:
|
| 20 |
+
return Image.new('RGB', (1, 1))
|
| 21 |
+
finally:
|
| 22 |
+
if wd:
|
| 23 |
+
wd.quit()
|
| 24 |
+
|
| 25 |
+
return Image.open(BytesIO(screenshot))
|
| 26 |
+
|
| 27 |
+
iface = gr.Interface(
|
| 28 |
+
fn=take_screenshot,
|
| 29 |
+
inputs=gr.inputs.Textbox(label="Website URL", default="https://kargaranamir.github.io"),
|
| 30 |
+
outputs=gr.Image(type="pil", height=360, width=540), # Adjust the image size here
|
| 31 |
+
title="Website Screenshot",
|
| 32 |
+
description="Take a screenshot of a website.",
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
iface.launch()
|
srt.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ffmpeg
|
| 2 |
+
|
| 3 |
+
input = ffmpeg.input('ninja300123.mp4')
|
| 4 |
+
output = ffmpeg.output(input, 'outputxd.mp4', vf="drawtext=text='Hola Mundo':x=10:y=H-th-10:fontsize=24:fontcolor=white")
|
| 5 |
+
output.run()
|
upload.py
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
`tiktok_uploader` module for uploading videos to TikTok
|
| 3 |
+
|
| 4 |
+
Key Functions
|
| 5 |
+
-------------
|
| 6 |
+
upload_video : Uploads a single TikTok video
|
| 7 |
+
upload_videos : Uploads multiple TikTok videos
|
| 8 |
+
"""
|
| 9 |
+
from os.path import abspath, exists
|
| 10 |
+
from typing import List
|
| 11 |
+
import time
|
| 12 |
+
import pytz
|
| 13 |
+
import datetime
|
| 14 |
+
|
| 15 |
+
from selenium.webdriver.common.by import By
|
| 16 |
+
|
| 17 |
+
from selenium.webdriver.common.action_chains import ActionChains
|
| 18 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
| 19 |
+
from selenium.webdriver.support import expected_conditions as EC
|
| 20 |
+
from selenium.webdriver.common.keys import Keys
|
| 21 |
+
|
| 22 |
+
from tiktok_uploader.browsers import get_browser
|
| 23 |
+
from tiktok_uploader.auth import AuthBackend
|
| 24 |
+
from tiktok_uploader import logger
|
| 25 |
+
from tiktok_uploader.utils import bold, green
|
| 26 |
+
import toml
|
| 27 |
+
config = toml.load("./config.toml")
|
| 28 |
+
from proxy_auth_extension.proxy_auth_extension import proxy_is_working
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def upload_video(filename=None, description='', schedule: datetime.datetime = None, username='',
|
| 32 |
+
password='', cookies='', sessionid=None, cookies_list=None, cookies_str=None, proxy=None, *args, **kwargs):
|
| 33 |
+
"""
|
| 34 |
+
Uploads a single TikTok video.
|
| 35 |
+
|
| 36 |
+
Conder using `upload_videos` if using multiple videos
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
filename : str
|
| 41 |
+
The path to the video to upload
|
| 42 |
+
description : str
|
| 43 |
+
The description to set for the video
|
| 44 |
+
schedule: datetime.datetime
|
| 45 |
+
The datetime to schedule the video, must be naive or aware with UTC timezone, if naive it will be aware with UTC timezone
|
| 46 |
+
cookies : str
|
| 47 |
+
The cookies to use for uploading
|
| 48 |
+
sessionid: str
|
| 49 |
+
The `sessionid` is the only required cookie for uploading,
|
| 50 |
+
but it is recommended to use all cookies to avoid detection
|
| 51 |
+
"""
|
| 52 |
+
auth = AuthBackend(username=username, password=password, cookies=cookies,
|
| 53 |
+
cookies_list=cookies_list, cookies_str=cookies_str, sessionid=sessionid)
|
| 54 |
+
|
| 55 |
+
return upload_videos(
|
| 56 |
+
videos=[ { 'path': filename, 'description': description, 'schedule': schedule } ],
|
| 57 |
+
auth=auth,
|
| 58 |
+
proxy=proxy,
|
| 59 |
+
*args, **kwargs
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def upload_videos(videos: list = None, auth: AuthBackend = None, proxy: dict = None, browser='chrome',
|
| 64 |
+
browser_agent=None, on_complete=None, headless=True, num_retires : int = 1, *args, **kwargs):
|
| 65 |
+
"""
|
| 66 |
+
Uploads multiple videos to TikTok
|
| 67 |
+
|
| 68 |
+
Parameters
|
| 69 |
+
----------
|
| 70 |
+
videos : list
|
| 71 |
+
A list of dictionaries containing the video's ('path') and description ('description')
|
| 72 |
+
proxy: dict
|
| 73 |
+
A dictionary containing the proxy user, pass, host and port
|
| 74 |
+
browser : str
|
| 75 |
+
The browser to use for uploading
|
| 76 |
+
browser_agent : selenium.webdriver
|
| 77 |
+
A selenium webdriver object to use for uploading
|
| 78 |
+
on_complete : function
|
| 79 |
+
A function to call when the upload is complete
|
| 80 |
+
headless : bool
|
| 81 |
+
Whether or not the browser should be run in headless mode
|
| 82 |
+
num_retries : int
|
| 83 |
+
The number of retries to attempt if the upload fails
|
| 84 |
+
options : SeleniumOptions
|
| 85 |
+
The options to pass into the browser -> custom privacy settings, etc.
|
| 86 |
+
*args :
|
| 87 |
+
Additional arguments to pass into the upload function
|
| 88 |
+
**kwargs :
|
| 89 |
+
Additional keyword arguments to pass into the upload function
|
| 90 |
+
|
| 91 |
+
Returns
|
| 92 |
+
-------
|
| 93 |
+
failed : list
|
| 94 |
+
A list of videos which failed to upload
|
| 95 |
+
"""
|
| 96 |
+
videos = _convert_videos_dict(videos)
|
| 97 |
+
|
| 98 |
+
if videos and len(videos) > 1:
|
| 99 |
+
logger.debug("Uploading %d videos", len(videos))
|
| 100 |
+
|
| 101 |
+
if not browser_agent: # user-specified browser agent
|
| 102 |
+
logger.debug('Create a %s browser instance %s', browser,
|
| 103 |
+
'in headless mode' if headless else '')
|
| 104 |
+
driver = get_browser(name=browser, headless=headless, proxy=proxy, *args, **kwargs)
|
| 105 |
+
else:
|
| 106 |
+
logger.debug('Using user-defined browser agent')
|
| 107 |
+
driver = browser_agent
|
| 108 |
+
if proxy:
|
| 109 |
+
if proxy_is_working(driver, proxy['host']):
|
| 110 |
+
logger.debug(green('Proxy is working'))
|
| 111 |
+
else:
|
| 112 |
+
logger.error('Proxy is not working')
|
| 113 |
+
driver.quit()
|
| 114 |
+
raise Exception('Proxy is not working')
|
| 115 |
+
driver = auth.authenticate_agent(driver)
|
| 116 |
+
|
| 117 |
+
failed = []
|
| 118 |
+
# uploads each video
|
| 119 |
+
for video in videos:
|
| 120 |
+
try:
|
| 121 |
+
path = abspath(video.get('path'))
|
| 122 |
+
description = video.get('description', '')
|
| 123 |
+
schedule = video.get('schedule', None)
|
| 124 |
+
|
| 125 |
+
logger.debug('Posting %s%s', bold(video.get('path')),
|
| 126 |
+
f'\n{" " * 15}with description: {bold(description)}' if description else '')
|
| 127 |
+
|
| 128 |
+
# Video must be of supported type
|
| 129 |
+
if not _check_valid_path(path):
|
| 130 |
+
print(f'{path} is invalid, skipping')
|
| 131 |
+
failed.append(video)
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
# Video must have a valid datetime for tiktok's scheduler
|
| 135 |
+
if schedule:
|
| 136 |
+
timezone = pytz.UTC
|
| 137 |
+
if schedule.tzinfo is None:
|
| 138 |
+
schedule = schedule.astimezone(timezone)
|
| 139 |
+
elif int(schedule.utcoffset().total_seconds()) == 0: # Equivalent to UTC
|
| 140 |
+
schedule = timezone.localize(schedule)
|
| 141 |
+
else:
|
| 142 |
+
print(f'{schedule} is invalid, the schedule datetime must be naive or aware with UTC timezone, skipping')
|
| 143 |
+
failed.append(video)
|
| 144 |
+
continue
|
| 145 |
+
|
| 146 |
+
valid_tiktok_minute_multiple = 5
|
| 147 |
+
schedule = _get_valid_schedule_minute(schedule, valid_tiktok_minute_multiple)
|
| 148 |
+
if not _check_valid_schedule(schedule):
|
| 149 |
+
print(f'{schedule} is invalid, the schedule datetime must be as least 20 minutes in the future, and a maximum of 10 days, skipping')
|
| 150 |
+
failed.append(video)
|
| 151 |
+
continue
|
| 152 |
+
|
| 153 |
+
complete_upload_form(driver, path, description, schedule,
|
| 154 |
+
num_retires=num_retires, headless=headless,
|
| 155 |
+
*args, **kwargs)
|
| 156 |
+
except Exception as exception:
|
| 157 |
+
logger.error('Failed to upload %s', path)
|
| 158 |
+
logger.error(exception)
|
| 159 |
+
failed.append(video)
|
| 160 |
+
|
| 161 |
+
if on_complete is callable: # calls the user-specified on-complete function
|
| 162 |
+
on_complete(video)
|
| 163 |
+
|
| 164 |
+
if config['quit_on_end']:
|
| 165 |
+
driver.quit()
|
| 166 |
+
|
| 167 |
+
return failed
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def complete_upload_form(driver, path: str, description: str, schedule: datetime.datetime, headless=True, *args, **kwargs) -> None:
|
| 171 |
+
"""
|
| 172 |
+
Actually uploades each video
|
| 173 |
+
|
| 174 |
+
Parameters
|
| 175 |
+
----------
|
| 176 |
+
driver : selenium.webdriver
|
| 177 |
+
The selenium webdriver to use for uploading
|
| 178 |
+
path : str
|
| 179 |
+
The path to the video to upload
|
| 180 |
+
"""
|
| 181 |
+
_go_to_upload(driver)
|
| 182 |
+
_set_video(driver, path=path, **kwargs)
|
| 183 |
+
_set_interactivity(driver, **kwargs)
|
| 184 |
+
_set_description(driver, description)
|
| 185 |
+
if schedule:
|
| 186 |
+
_set_schedule_video(driver, schedule)
|
| 187 |
+
_post_video(driver)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _go_to_upload(driver) -> None:
|
| 191 |
+
"""
|
| 192 |
+
Navigates to the upload page, switches to the iframe and waits for it to load
|
| 193 |
+
|
| 194 |
+
Parameters
|
| 195 |
+
----------
|
| 196 |
+
driver : selenium.webdriver
|
| 197 |
+
"""
|
| 198 |
+
logger.debug(green('Navigating to upload page'))
|
| 199 |
+
|
| 200 |
+
driver.get(config['paths']['upload'])
|
| 201 |
+
|
| 202 |
+
# changes to the iframe
|
| 203 |
+
iframe_selector = EC.presence_of_element_located(
|
| 204 |
+
(By.XPATH, config['selectors']['upload']['iframe'])
|
| 205 |
+
)
|
| 206 |
+
iframe = WebDriverWait(driver, config['explicit_wait']).until(iframe_selector)
|
| 207 |
+
driver.switch_to.frame(iframe)
|
| 208 |
+
|
| 209 |
+
# waits for the iframe to load
|
| 210 |
+
root_selector = EC.presence_of_element_located((By.ID, 'root'))
|
| 211 |
+
WebDriverWait(driver, config['explicit_wait']).until(root_selector)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _set_description(driver, description: str) -> None:
|
| 215 |
+
"""
|
| 216 |
+
Sets the description of the video
|
| 217 |
+
|
| 218 |
+
Parameters
|
| 219 |
+
----------
|
| 220 |
+
driver : selenium.webdriver
|
| 221 |
+
description : str
|
| 222 |
+
The description to set
|
| 223 |
+
"""
|
| 224 |
+
if description is None:
|
| 225 |
+
# if no description is provided, filename
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
logger.debug(green('Setting description'))
|
| 229 |
+
|
| 230 |
+
saved_description = description # save the description in case it fails
|
| 231 |
+
|
| 232 |
+
desc = driver.find_element(By.XPATH, config['selectors']['upload']['description'])
|
| 233 |
+
|
| 234 |
+
# desc populates with filename before clearing
|
| 235 |
+
WebDriverWait(driver, config['explicit_wait']).until(lambda driver: desc.text != '')
|
| 236 |
+
|
| 237 |
+
_clear(desc)
|
| 238 |
+
|
| 239 |
+
try:
|
| 240 |
+
while description:
|
| 241 |
+
nearest_mention = description.find('@')
|
| 242 |
+
nearest_hash = description.find('#')
|
| 243 |
+
|
| 244 |
+
if nearest_mention == 0 or nearest_hash == 0:
|
| 245 |
+
desc.send_keys('@' if nearest_mention == 0 else '#')
|
| 246 |
+
|
| 247 |
+
# wait for the frames to load
|
| 248 |
+
time.sleep(config['implicit_wait'])
|
| 249 |
+
|
| 250 |
+
name = description[1:].split(' ')[0]
|
| 251 |
+
if nearest_mention == 0: # @ case
|
| 252 |
+
mention_xpath = config['selectors']['upload']['mention_box']
|
| 253 |
+
condition = EC.presence_of_element_located((By.XPATH, mention_xpath))
|
| 254 |
+
mention_box = WebDriverWait(driver, config['explicit_wait']).until(condition)
|
| 255 |
+
mention_box.send_keys(name)
|
| 256 |
+
else:
|
| 257 |
+
desc.send_keys(name)
|
| 258 |
+
|
| 259 |
+
time.sleep(config['implicit_wait'])
|
| 260 |
+
|
| 261 |
+
if nearest_mention == 0: # @ case
|
| 262 |
+
mention_xpath = config['selectors']['upload']['mentions'].format('@' + name)
|
| 263 |
+
condition = EC.presence_of_element_located((By.XPATH, mention_xpath))
|
| 264 |
+
else:
|
| 265 |
+
hashtag_xpath = config['selectors']['upload']['hashtags'].format(name)
|
| 266 |
+
condition = EC.presence_of_element_located((By.XPATH, hashtag_xpath))
|
| 267 |
+
|
| 268 |
+
elem = WebDriverWait(driver, config['explicit_wait']).until(condition)
|
| 269 |
+
|
| 270 |
+
ActionChains(driver).move_to_element(elem).click(elem).perform()
|
| 271 |
+
|
| 272 |
+
description = description[len(name) + 2:]
|
| 273 |
+
else:
|
| 274 |
+
min_index = _get_splice_index(nearest_mention, nearest_hash, description)
|
| 275 |
+
|
| 276 |
+
desc.send_keys(description[:min_index])
|
| 277 |
+
description = description[min_index:]
|
| 278 |
+
except Exception as exception:
|
| 279 |
+
print('Failed to set description: ', exception)
|
| 280 |
+
_clear(desc)
|
| 281 |
+
desc.send_keys(saved_description) # if fail, use saved description
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _clear(element) -> None:
|
| 285 |
+
"""
|
| 286 |
+
Clears the text of the element (an issue with the TikTok website when automating)
|
| 287 |
+
|
| 288 |
+
Parameters
|
| 289 |
+
----------
|
| 290 |
+
element
|
| 291 |
+
The text box to clear
|
| 292 |
+
"""
|
| 293 |
+
element.send_keys(2 * len(element.text) * Keys.BACKSPACE)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def _set_video(driver, path: str = '', num_retries: int = 3, **kwargs) -> None:
|
| 297 |
+
"""
|
| 298 |
+
Sets the video to upload
|
| 299 |
+
|
| 300 |
+
Parameters
|
| 301 |
+
----------
|
| 302 |
+
driver : selenium.webdriver
|
| 303 |
+
path : str
|
| 304 |
+
The path to the video to upload
|
| 305 |
+
num_retries : number of retries (can occassionally fail)
|
| 306 |
+
"""
|
| 307 |
+
# uploades the element
|
| 308 |
+
logger.debug(green('Uploading video file'))
|
| 309 |
+
|
| 310 |
+
for _ in range(num_retries):
|
| 311 |
+
try:
|
| 312 |
+
logger.debug(green('Uploading video file 1'))
|
| 313 |
+
upload_box = driver.find_element(
|
| 314 |
+
By.XPATH, config['selectors']['upload']['upload_video']
|
| 315 |
+
)
|
| 316 |
+
logger.debug(green('Uploading video file 2'))
|
| 317 |
+
upload_box.send_keys(path)
|
| 318 |
+
logger.debug(green('Uploading video file 3'))
|
| 319 |
+
# waits for the upload progress bar to disappear
|
| 320 |
+
upload_progress = EC.presence_of_element_located(
|
| 321 |
+
(By.XPATH, config['selectors']['upload']['upload_in_progress'])
|
| 322 |
+
)
|
| 323 |
+
logger.debug(green('Uploading video file 4'))
|
| 324 |
+
WebDriverWait(driver, config['implicit_wait']).until(upload_progress)
|
| 325 |
+
WebDriverWait(driver, config['explicit_wait']).until_not(upload_progress)
|
| 326 |
+
logger.debug(green('Uploading video file 5'))
|
| 327 |
+
# waits for the video to upload
|
| 328 |
+
upload_confirmation = EC.presence_of_element_located(
|
| 329 |
+
(By.XPATH, config['selectors']['upload']['upload_confirmation'])
|
| 330 |
+
)
|
| 331 |
+
logger.debug(green('Uploading video file 6'))
|
| 332 |
+
# An exception throw here means the video failed to upload an a retry is needed
|
| 333 |
+
WebDriverWait(driver, config['explicit_wait']).until(upload_confirmation)
|
| 334 |
+
logger.debug(green('Uploading video file 7'))
|
| 335 |
+
# wait until a non-draggable image is found
|
| 336 |
+
process_confirmation = EC.presence_of_element_located(
|
| 337 |
+
(By.XPATH, config['selectors']['upload']['process_confirmation'])
|
| 338 |
+
)
|
| 339 |
+
logger.debug(green('Uploading video file 8'))
|
| 340 |
+
WebDriverWait(driver, config['explicit_wait']).until(process_confirmation)
|
| 341 |
+
logger.debug(green('Uploading video file 9'))
|
| 342 |
+
return
|
| 343 |
+
except Exception as exception:
|
| 344 |
+
print(exception)
|
| 345 |
+
|
| 346 |
+
raise FailedToUpload()
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def _set_interactivity(driver, comment=True, stitch=True, duet=True, *args, **kwargs) -> None:
|
| 350 |
+
"""
|
| 351 |
+
Sets the interactivity settings of the video
|
| 352 |
+
|
| 353 |
+
Parameters
|
| 354 |
+
----------
|
| 355 |
+
driver : selenium.webdriver
|
| 356 |
+
comment : bool
|
| 357 |
+
Whether or not to allow comments
|
| 358 |
+
stitch : bool
|
| 359 |
+
Whether or not to allow stitching
|
| 360 |
+
duet : bool
|
| 361 |
+
Whether or not to allow duets
|
| 362 |
+
"""
|
| 363 |
+
try:
|
| 364 |
+
logger.debug(green('Setting interactivity settings'))
|
| 365 |
+
|
| 366 |
+
comment_box = driver.find_element(By.XPATH, config['selectors']['upload']['comment'])
|
| 367 |
+
stitch_box = driver.find_element(By.XPATH, config['selectors']['upload']['stitch'])
|
| 368 |
+
duet_box = driver.find_element(By.XPATH, config['selectors']['upload']['duet'])
|
| 369 |
+
|
| 370 |
+
# xor the current state with the desired state
|
| 371 |
+
if comment ^ comment_box.is_selected():
|
| 372 |
+
comment_box.click()
|
| 373 |
+
|
| 374 |
+
if stitch ^ stitch_box.is_selected():
|
| 375 |
+
stitch_box.click()
|
| 376 |
+
|
| 377 |
+
if duet ^ duet_box.is_selected():
|
| 378 |
+
duet_box.click()
|
| 379 |
+
|
| 380 |
+
except Exception as _:
|
| 381 |
+
logger.error('Failed to set interactivity settings')
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def _set_schedule_video(driver, schedule: datetime.datetime) -> None:
|
| 385 |
+
"""
|
| 386 |
+
Sets the schedule of the video
|
| 387 |
+
|
| 388 |
+
Parameters
|
| 389 |
+
----------
|
| 390 |
+
driver : selenium.webdriver
|
| 391 |
+
schedule : datetime.datetime
|
| 392 |
+
The datetime to set
|
| 393 |
+
"""
|
| 394 |
+
|
| 395 |
+
logger.debug(green('Setting schedule'))
|
| 396 |
+
|
| 397 |
+
driver_timezone = __get_driver_timezone(driver)
|
| 398 |
+
schedule = schedule.astimezone(driver_timezone)
|
| 399 |
+
|
| 400 |
+
month = schedule.month
|
| 401 |
+
day = schedule.day
|
| 402 |
+
hour = schedule.hour
|
| 403 |
+
minute = schedule.minute
|
| 404 |
+
|
| 405 |
+
try:
|
| 406 |
+
switch = driver.find_element(By.XPATH, config['selectors']['schedule']['switch'])
|
| 407 |
+
switch.click()
|
| 408 |
+
__date_picker(driver, month, day)
|
| 409 |
+
__time_picker(driver, hour, minute)
|
| 410 |
+
except Exception as e:
|
| 411 |
+
msg = f'Failed to set schedule: {e}'
|
| 412 |
+
print(msg)
|
| 413 |
+
logger.error(msg)
|
| 414 |
+
raise FailedToUpload()
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def __date_picker(driver, month: int, day: int) -> None:
|
| 419 |
+
logger.debug(green('Picking date'))
|
| 420 |
+
|
| 421 |
+
condition = EC.presence_of_element_located(
|
| 422 |
+
(By.XPATH, config['selectors']['schedule']['date_picker'])
|
| 423 |
+
)
|
| 424 |
+
date_picker = WebDriverWait(driver, config['implicit_wait']).until(condition)
|
| 425 |
+
date_picker.click()
|
| 426 |
+
|
| 427 |
+
condition = EC.presence_of_element_located(
|
| 428 |
+
(By.XPATH, config['selectors']['schedule']['calendar'])
|
| 429 |
+
)
|
| 430 |
+
calendar = WebDriverWait(driver, config['implicit_wait']).until(condition)
|
| 431 |
+
|
| 432 |
+
calendar_month = driver.find_element(By.XPATH, config['selectors']['schedule']['calendar_month']).text
|
| 433 |
+
n_calendar_month = datetime.datetime.strptime(calendar_month, '%B').month
|
| 434 |
+
if n_calendar_month != month: # Max can be a month before or after
|
| 435 |
+
if n_calendar_month < month:
|
| 436 |
+
arrow = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_arrows'])[-1]
|
| 437 |
+
else:
|
| 438 |
+
arrow = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_arrows'])[0]
|
| 439 |
+
arrow.click()
|
| 440 |
+
valid_days = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_valid_days'])
|
| 441 |
+
|
| 442 |
+
day_to_click = None
|
| 443 |
+
for day_option in valid_days:
|
| 444 |
+
if int(day_option.text) == day:
|
| 445 |
+
day_to_click = day_option
|
| 446 |
+
break
|
| 447 |
+
if day_to_click:
|
| 448 |
+
day_to_click.click()
|
| 449 |
+
else:
|
| 450 |
+
raise Exception('Day not found in calendar')
|
| 451 |
+
|
| 452 |
+
__verify_date_picked_is_correct(driver, month, day)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def __verify_date_picked_is_correct(driver, month: int, day: int):
|
| 456 |
+
date_selected = driver.find_element(By.XPATH, config['selectors']['schedule']['date_picker']).text
|
| 457 |
+
date_selected_month = int(date_selected.split('-')[1])
|
| 458 |
+
date_selected_day = int(date_selected.split('-')[2])
|
| 459 |
+
|
| 460 |
+
if date_selected_month == month and date_selected_day == day:
|
| 461 |
+
logger.debug(green('Date picked correctly'))
|
| 462 |
+
else:
|
| 463 |
+
msg = f'Something went wrong with the date picker, expected {month}-{day} but got {date_selected_month}-{date_selected_day}'
|
| 464 |
+
logger.error(msg)
|
| 465 |
+
raise Exception(msg)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def __time_picker(driver, hour: int, minute: int) -> None:
|
| 469 |
+
logger.debug(green('Picking time'))
|
| 470 |
+
|
| 471 |
+
condition = EC.presence_of_element_located(
|
| 472 |
+
(By.XPATH, config['selectors']['schedule']['time_picker'])
|
| 473 |
+
)
|
| 474 |
+
time_picker = WebDriverWait(driver, config['implicit_wait']).until(condition)
|
| 475 |
+
time_picker.click()
|
| 476 |
+
|
| 477 |
+
condition = EC.presence_of_element_located(
|
| 478 |
+
(By.XPATH, config['selectors']['schedule']['time_picker_container'])
|
| 479 |
+
)
|
| 480 |
+
time_picker_container = WebDriverWait(driver, config['implicit_wait']).until(condition)
|
| 481 |
+
|
| 482 |
+
# 00 = 0, 01 = 1, 02 = 2, 03 = 3, 04 = 4, 05 = 5, 06 = 6, 07 = 7, 08 = 8, 09 = 9, 10 = 10, 11 = 11, 12 = 12,
|
| 483 |
+
# 13 = 13, 14 = 14, 15 = 15, 16 = 16, 17 = 17, 18 = 18, 19 = 19, 20 = 20, 21 = 21, 22 = 22, 23 = 23
|
| 484 |
+
hour_options = driver.find_elements(By.XPATH, config['selectors']['schedule']['timepicker_hours'])
|
| 485 |
+
# 00 == 0, 05 == 1, 10 == 2, 15 == 3, 20 == 4, 25 == 5, 30 == 6, 35 == 7, 40 == 8, 45 == 9, 50 == 10, 55 == 11
|
| 486 |
+
minute_options = driver.find_elements(By.XPATH, config['selectors']['schedule']['timepicker_minutes'])
|
| 487 |
+
|
| 488 |
+
hour_to_click = hour_options[hour]
|
| 489 |
+
minute_option_correct_index = int(minute / 5)
|
| 490 |
+
minute_to_click = minute_options[minute_option_correct_index]
|
| 491 |
+
|
| 492 |
+
driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", hour_to_click)
|
| 493 |
+
hour_to_click.click()
|
| 494 |
+
driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", minute_to_click)
|
| 495 |
+
minute_to_click.click()
|
| 496 |
+
|
| 497 |
+
# click somewhere else to close the time picker
|
| 498 |
+
time_picker.click()
|
| 499 |
+
|
| 500 |
+
time.sleep(.5) # wait for the DOM change
|
| 501 |
+
__verify_time_picked_is_correct(driver, hour, minute)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def __verify_time_picked_is_correct(driver, hour: int, minute: int):
|
| 505 |
+
time_selected = driver.find_element(By.XPATH, config['selectors']['schedule']['time_picker_text']).text
|
| 506 |
+
time_selected_hour = int(time_selected.split(':')[0])
|
| 507 |
+
time_selected_minute = int(time_selected.split(':')[1])
|
| 508 |
+
|
| 509 |
+
if time_selected_hour == hour and time_selected_minute == minute:
|
| 510 |
+
logger.debug(green('Time picked correctly'))
|
| 511 |
+
else:
|
| 512 |
+
msg = f'Something went wrong with the time picker, ' \
|
| 513 |
+
f'expected {hour:02d}:{minute:02d} ' \
|
| 514 |
+
f'but got {time_selected_hour:02d}:{time_selected_minute:02d}'
|
| 515 |
+
logger.error(msg)
|
| 516 |
+
raise Exception(msg)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def _post_video(driver) -> None:
|
| 520 |
+
"""
|
| 521 |
+
Posts the video by clicking the post button
|
| 522 |
+
|
| 523 |
+
Parameters
|
| 524 |
+
----------
|
| 525 |
+
driver : selenium.webdriver
|
| 526 |
+
"""
|
| 527 |
+
logger.debug(green('Clicking the post button'))
|
| 528 |
+
|
| 529 |
+
post = driver.find_element(By.XPATH, config['selectors']['upload']['post'])
|
| 530 |
+
post.click()
|
| 531 |
+
|
| 532 |
+
# waits for the video to upload
|
| 533 |
+
post_confirmation = EC.presence_of_element_located(
|
| 534 |
+
(By.XPATH, config['selectors']['upload']['post_confirmation'])
|
| 535 |
+
)
|
| 536 |
+
WebDriverWait(driver, config['explicit_wait']).until(post_confirmation)
|
| 537 |
+
|
| 538 |
+
logger.debug(green('Video posted successfully'))
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
# HELPERS
|
| 542 |
+
|
| 543 |
+
def _check_valid_path(path: str) -> bool:
|
| 544 |
+
"""
|
| 545 |
+
Returns whether or not the filetype is supported by TikTok
|
| 546 |
+
"""
|
| 547 |
+
return exists(path) and path.split('.')[-1] in config['supported_file_types']
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def _get_valid_schedule_minute(schedule, valid_multiple) -> datetime.datetime:
|
| 551 |
+
"""
|
| 552 |
+
Returns a datetime.datetime with valid minute for TikTok
|
| 553 |
+
"""
|
| 554 |
+
if _is_valid_schedule_minute(schedule.minute, valid_multiple):
|
| 555 |
+
return schedule
|
| 556 |
+
else:
|
| 557 |
+
return _set_valid_schedule_minute(schedule, valid_multiple)
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
def _is_valid_schedule_minute(minute, valid_multiple) -> bool:
|
| 561 |
+
if minute % valid_multiple != 0:
|
| 562 |
+
return False
|
| 563 |
+
else:
|
| 564 |
+
return True
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def _set_valid_schedule_minute(schedule, valid_multiple) -> datetime.datetime:
|
| 568 |
+
minute = schedule.minute
|
| 569 |
+
|
| 570 |
+
remainder = minute % valid_multiple
|
| 571 |
+
integers_to_valid_multiple = 5 - remainder
|
| 572 |
+
schedule += datetime.timedelta(minutes=integers_to_valid_multiple)
|
| 573 |
+
|
| 574 |
+
return schedule
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def _check_valid_schedule(schedule: datetime.datetime) -> bool:
|
| 578 |
+
"""
|
| 579 |
+
Returns if the schedule is supported by TikTok
|
| 580 |
+
"""
|
| 581 |
+
valid_tiktok_minute_multiple = 5
|
| 582 |
+
margin_to_complete_upload_form = 5
|
| 583 |
+
|
| 584 |
+
datetime_utc_now = pytz.UTC.localize(datetime.datetime.utcnow())
|
| 585 |
+
min_datetime_tiktok_valid = datetime_utc_now + datetime.timedelta(minutes=15)
|
| 586 |
+
min_datetime_tiktok_valid += datetime.timedelta(minutes=margin_to_complete_upload_form)
|
| 587 |
+
max_datetime_tiktok_valid = datetime_utc_now + datetime.timedelta(days=10)
|
| 588 |
+
if schedule < min_datetime_tiktok_valid \
|
| 589 |
+
or schedule > max_datetime_tiktok_valid:
|
| 590 |
+
return False
|
| 591 |
+
elif not _is_valid_schedule_minute(schedule.minute, valid_tiktok_minute_multiple):
|
| 592 |
+
return False
|
| 593 |
+
else:
|
| 594 |
+
return True
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _get_splice_index(nearest_mention: int, nearest_hashtag: int, description: str) -> int:
|
| 598 |
+
"""
|
| 599 |
+
Returns the index to splice the description at
|
| 600 |
+
|
| 601 |
+
Parameters
|
| 602 |
+
----------
|
| 603 |
+
nearest_mention : int
|
| 604 |
+
The index of the nearest mention
|
| 605 |
+
nearest_hashtag : int
|
| 606 |
+
The index of the nearest hashtag
|
| 607 |
+
|
| 608 |
+
Returns
|
| 609 |
+
-------
|
| 610 |
+
int
|
| 611 |
+
The index to splice the description at
|
| 612 |
+
"""
|
| 613 |
+
if nearest_mention == -1 and nearest_hashtag == -1:
|
| 614 |
+
return len(description)
|
| 615 |
+
elif nearest_hashtag == -1:
|
| 616 |
+
return nearest_mention
|
| 617 |
+
elif nearest_mention == -1:
|
| 618 |
+
return nearest_hashtag
|
| 619 |
+
else:
|
| 620 |
+
return min(nearest_mention, nearest_hashtag)
|
| 621 |
+
|
| 622 |
+
def _convert_videos_dict(videos_list_of_dictionaries) -> List:
|
| 623 |
+
"""
|
| 624 |
+
Takes in a videos dictionary and converts it.
|
| 625 |
+
|
| 626 |
+
This allows the user to use the wrong stuff and thing to just work
|
| 627 |
+
"""
|
| 628 |
+
if not videos_list_of_dictionaries:
|
| 629 |
+
raise RuntimeError("No videos to upload")
|
| 630 |
+
|
| 631 |
+
valid_path = config['valid_path_names']
|
| 632 |
+
valid_description = config['valid_descriptions']
|
| 633 |
+
|
| 634 |
+
correct_path = valid_path[0]
|
| 635 |
+
correct_description = valid_description[0]
|
| 636 |
+
|
| 637 |
+
def intersection(lst1, lst2):
|
| 638 |
+
""" return the intersection of two lists """
|
| 639 |
+
return list(set(lst1) & set(lst2))
|
| 640 |
+
|
| 641 |
+
return_list = []
|
| 642 |
+
for elem in videos_list_of_dictionaries:
|
| 643 |
+
# preprocesses the dictionary
|
| 644 |
+
elem = {k.strip().lower(): v for k, v in elem.items()}
|
| 645 |
+
|
| 646 |
+
keys = elem.keys()
|
| 647 |
+
path_intersection = intersection(valid_path, keys)
|
| 648 |
+
description_interesection = intersection(valid_description, keys)
|
| 649 |
+
|
| 650 |
+
if path_intersection:
|
| 651 |
+
# we have a path
|
| 652 |
+
path = elem[path_intersection.pop()]
|
| 653 |
+
|
| 654 |
+
if not _check_valid_path(path):
|
| 655 |
+
raise RuntimeError("Invalid path: " + path)
|
| 656 |
+
|
| 657 |
+
elem[correct_path] = path
|
| 658 |
+
else:
|
| 659 |
+
# iterates over the elem and find a key which is a path with a valid extension
|
| 660 |
+
for _, value in elem.items():
|
| 661 |
+
if _check_valid_path(value):
|
| 662 |
+
elem[correct_path] = value
|
| 663 |
+
break
|
| 664 |
+
else:
|
| 665 |
+
# no valid path found
|
| 666 |
+
raise RuntimeError("Path not found in dictionary: " + str(elem))
|
| 667 |
+
|
| 668 |
+
if description_interesection:
|
| 669 |
+
# we have a description
|
| 670 |
+
elem[correct_description] = elem[description_interesection.pop()]
|
| 671 |
+
else:
|
| 672 |
+
# iterates over the elem and finds a description which is not a valid path
|
| 673 |
+
for _, value in elem.items():
|
| 674 |
+
if not _check_valid_path(value):
|
| 675 |
+
elem[correct_description] = value
|
| 676 |
+
break
|
| 677 |
+
else:
|
| 678 |
+
elem[correct_description] = '' # null description is fine
|
| 679 |
+
|
| 680 |
+
return_list.append(elem)
|
| 681 |
+
|
| 682 |
+
return return_list
|
| 683 |
+
|
| 684 |
+
def __get_driver_timezone(driver) -> pytz.timezone:
|
| 685 |
+
"""
|
| 686 |
+
Returns the timezone of the driver
|
| 687 |
+
"""
|
| 688 |
+
timezone_str = driver.execute_script("return Intl.DateTimeFormat().resolvedOptions().timeZone")
|
| 689 |
+
return pytz.timezone(timezone_str)
|
| 690 |
+
|
| 691 |
+
class DescriptionTooLong(Exception):
|
| 692 |
+
"""
|
| 693 |
+
A video description longer than the maximum allowed by TikTok's website (not app) uploader
|
| 694 |
+
"""
|
| 695 |
+
|
| 696 |
+
def __init__(self, message=None):
|
| 697 |
+
super().__init__(message or self.__doc__)
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
class FailedToUpload(Exception):
|
| 701 |
+
"""
|
| 702 |
+
A video failed to upload
|
| 703 |
+
"""
|
| 704 |
+
|
| 705 |
+
def __init__(self, message=None):
|
| 706 |
+
super().__init__(message or self.__doc__)
|
utils.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydub import AudioSegment
|
| 2 |
+
#from pydub.utils import mediainfo
|
| 3 |
+
from pydub.utils import make_chunks
|
| 4 |
+
import math
|
| 5 |
+
#flac_audio = AudioSegment.from_file("sample.flac", "flac")
|
| 6 |
+
#flac_audio.export("audio.wav", format="wav")
|
| 7 |
+
def split_audio_wav(filename):
|
| 8 |
+
myaudio = AudioSegment.from_file(filename , "wav")
|
| 9 |
+
channel_count = myaudio.channels #Get channels
|
| 10 |
+
sample_width = myaudio.sample_width #Get sample width
|
| 11 |
+
duration_in_sec = len(myaudio) / 1000#Length of audio in sec
|
| 12 |
+
sample_rate = myaudio.frame_rate
|
| 13 |
+
print("sample_width=", sample_width)
|
| 14 |
+
print("channel_count=", channel_count)
|
| 15 |
+
print("duration_in_sec=", duration_in_sec)
|
| 16 |
+
print("frame_rate=", sample_rate)
|
| 17 |
+
bit_rate =16 #assumption , you can extract from mediainfo("test.wav") dynamically
|
| 18 |
+
wav_file_size = (sample_rate * bit_rate * channel_count * duration_in_sec) / 8
|
| 19 |
+
print("wav_file_size = ",wav_file_size)
|
| 20 |
+
file_split_size = 40000000 # 40mb OR 40, 000, 000 bytes
|
| 21 |
+
total_chunks = wav_file_size // file_split_size
|
| 22 |
+
#Get chunk size by following method #There are more than one ofcourse
|
| 23 |
+
#for duration_in_sec (X) --> wav_file_size (Y)
|
| 24 |
+
#So whats duration in sec (K) --> for file size of 40Mb
|
| 25 |
+
# K = X * 40Mb / Y
|
| 26 |
+
chunk_length_in_sec = math.ceil((duration_in_sec * 40000000 ) /wav_file_size) #in sec
|
| 27 |
+
chunk_length_ms = chunk_length_in_sec * 1000
|
| 28 |
+
chunks = make_chunks(myaudio, chunk_length_ms)
|
| 29 |
+
number_chunks=len(chunks)
|
| 30 |
+
chunks_list=[]
|
| 31 |
+
#Export all of the individual chunks as wav files
|
| 32 |
+
for i, chunk in enumerate(chunks):
|
| 33 |
+
chunk_name = "chunk{0}.wav".format(i)
|
| 34 |
+
print("exporting", chunk_name)
|
| 35 |
+
chunk.export(chunk_name, format="wav")
|
| 36 |
+
chunks_list.append(chunk_name)
|
| 37 |
+
return chunks_list
|