Skip to content

Commit dbf6f54

Browse files
author
timhauke
committed
feat: Enhance driver robustness, add multi-tab support and ad handling
Refactor driver initialization to prioritize Selenium Manager with webdriver_manager as a fallback. Increase WebDriverWait timeout for improved handling of dynamic content and slow page loads. Implement multi-tab viewing functionality in main.py based on tab_amount in config.json. Add logic to accept cookies and skip ads for a smoother viewing experience. Introduce helper methods in the Bot class for better code organization and error handling. Update config.json with example values and README.md for clarity. Add .github/ directory for project automation. Remove Opera browser support.
1 parent 5588b85 commit dbf6f54

File tree

5 files changed

+183
-23
lines changed

5 files changed

+183
-23
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @gavintranquilino/maintainers @VectoDE

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pip install webdriver-manager selenium requests
3434

3535
```json
3636
{
37-
"website": "YOUR VIDEO",
37+
"website": "YOUR VIDEO",
3838
"tab_amount": 3,
3939
"watch_time": 35,
4040
"view_cycles": 5,

config.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"website": "YOUR VIDEO",
2+
"website": "your video url",
33
"tab_amount": 3,
4-
"watch_time": 35,
4+
"watch_time": 28,
55
"view_cycles": 5,
6-
"browser": "firefox"
6+
"browser": "chrome"
77
}

driver.py

Lines changed: 175 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,185 @@
33
from selenium.webdriver.common.by import By
44
from selenium.webdriver.support.ui import WebDriverWait
55
from selenium.webdriver.support import expected_conditions as EC
6+
from selenium.common.exceptions import (
7+
TimeoutException,
8+
InvalidSessionIdException,
9+
NoSuchWindowException,
10+
StaleElementReferenceException,
11+
ElementClickInterceptedException,
12+
)
613

714
from webdriver_manager.chrome import ChromeDriverManager
815
from webdriver_manager.firefox import GeckoDriverManager
916
from webdriver_manager.opera import OperaDriverManager
1017
from webdriver_manager.microsoft import IEDriverManager, EdgeChromiumDriverManager
18+
from selenium.webdriver.chrome.service import Service as ChromeService
19+
from selenium.webdriver.firefox.service import Service as FirefoxService
20+
from selenium.webdriver.ie.service import Service as IEService
21+
from selenium.webdriver.edge.service import Service as EdgeService
1122

1223
def get_driver(browser):
1324
"""Get the webdriver specified in configuration"""
1425

1526
# Browser name aliases
1627
chrome = ('chrome', 'google', 'google chrome', 'googlechrome', 'google-chrome', 'google_chrome')
1728
firefox = ('firefox', 'ff', 'mozilla', 'gecko', 'geckodriver', 'fire fox', 'fire_fox', 'fire-fox')
18-
opera = ('opera', 'opera gx', 'operagx', 'opera_gx', 'opera-gx')
1929
explorer = ('explorer', 'ie', 'internet explorer', 'internet-explorer', 'internet_explorer')
2030
edge = ('edge', 'microsoft edge', 'microsoft_edge', 'microsoft-edge')
2131

2232
# Download browser binaries according to settings.json
2333
if browser.lower() in chrome:
24-
return webdriver.Chrome(ChromeDriverManager().install())
34+
# Prefer built-in Selenium Manager; fallback to webdriver_manager only if needed
35+
try:
36+
return webdriver.Chrome()
37+
except Exception:
38+
service = ChromeService(ChromeDriverManager().install())
39+
return webdriver.Chrome(service=service)
2540

2641
elif browser.lower() in firefox:
27-
return webdriver.Firefox(executable_path=GeckoDriverManager().install())
28-
29-
elif browser.lower() in opera:
30-
return webdriver.Opera(OperaDriverManager().install())
42+
try:
43+
return webdriver.Firefox()
44+
except Exception:
45+
service = FirefoxService(GeckoDriverManager().install())
46+
return webdriver.Firefox(service=service)
3147

3248
elif browser.lower() in explorer:
33-
return webdriver.Ie(IEDriverManager().install())
49+
try:
50+
return webdriver.Ie()
51+
except Exception:
52+
service = IEService(IEDriverManager().install())
53+
return webdriver.Ie(service=service)
3454

3555
elif browser.lower() in edge:
36-
return webdriver.Edge(executable_path=EdgeChromiumDriverManager().install())
56+
try:
57+
return webdriver.Edge()
58+
except Exception:
59+
service = EdgeService(EdgeChromiumDriverManager().install())
60+
return webdriver.Edge(service=service)
3761

3862
else:
39-
raise RuntimeError('Browser not found {}'.format(browser.lower()))
63+
raise RuntimeError('Browser not found {}. Supported: Chrome, Firefox, Edge, IE'.format(browser.lower()))
4064

4165

4266
class Bot(object):
4367
"""Handle connections"""
4468

45-
def __init__(self, website, browser):
69+
def __init__(self, website, browser, tab_amount=1):
4670
"""Initial function"""
4771

72+
self.browser = browser
73+
self.tab_amount = tab_amount
4874
self.driver = get_driver(browser)
49-
self.website = website
50-
self.wait = WebDriverWait(self.driver, 3)
75+
self.website = website
76+
# Allow slower page loads/overlays before timing out
77+
self.wait = WebDriverWait(self.driver, 20)
5178
self.presence = EC.presence_of_element_located
5279
self.visible = EC.visibility_of_element_located
5380

5481
def get_vid(self):
5582
"""Open the YouTube link"""
5683

5784
self.driver.get(self.website)
58-
85+
self._accept_cookies()
86+
5987
def play_video(self):
6088
"""Click on the play button"""
6189

62-
self.wait.until(self.visible((By.ID, "video-title")))
63-
self.driver.find_element_by_xpath("//button[@class='ytp-large-play-button ytp-button']").click()
90+
# Ensure we are on the desired video (avoid autoplay chaining to another video)
91+
if self.website not in self.driver.current_url:
92+
self.driver.get(self.website)
93+
94+
# Wait for player to be ready and ensure play button is clickable
95+
self._accept_cookies()
96+
self.wait.until(self.presence((By.TAG_NAME, "video")))
97+
self._dismiss_overlays()
98+
if not self._click_play_button():
99+
# Fallback: try to start video via JS if button not found/clickable
100+
self.driver.execute_script("""
101+
const v = document.querySelector('video');
102+
if (v) { v.muted = false; v.play().catch(()=>{}); }
103+
""")
104+
105+
def _dismiss_overlays(self):
106+
"""Attempt to close common YouTube overlays such as cookie consent backdrops."""
107+
108+
# Remove overlay backdrops that block clicks
109+
self.driver.execute_script(
110+
"document.querySelectorAll('tp-yt-iron-overlay-backdrop').forEach(e => e.remove());"
111+
)
112+
113+
# Try to click consent buttons if present
114+
consent_selectors = [
115+
"//button[normalize-space()='I agree']",
116+
"//button[normalize-space()='Accept all']",
117+
"//button[normalize-space()='Alle akzeptieren']",
118+
"//button[normalize-space()='Ich stimme zu']",
119+
]
120+
121+
for selector in consent_selectors:
122+
try:
123+
btn = WebDriverWait(self.driver, 3).until(
124+
EC.element_to_be_clickable((By.XPATH, selector))
125+
)
126+
btn.click()
127+
break
128+
except TimeoutException:
129+
continue
130+
131+
def _accept_cookies(self):
132+
"""Accept YouTube/Google cookie banners if present."""
133+
134+
consent_selectors = [
135+
"//button[normalize-space()='I agree']",
136+
"//button[normalize-space()='Accept all']",
137+
"//button[normalize-space()='Alle akzeptieren']",
138+
"//button[normalize-space()='Ich stimme zu']",
139+
"//button[normalize-space()='Zustimmen und fortfahren']",
140+
"//button[@aria-label='Accept all']",
141+
"//button[@aria-label='Alle akzeptieren']",
142+
]
143+
144+
try:
145+
WebDriverWait(self.driver, 5).until(lambda d: True)
146+
for selector in consent_selectors:
147+
buttons = self.driver.find_elements(By.XPATH, selector)
148+
for btn in buttons:
149+
if btn.is_displayed() and btn.is_enabled():
150+
btn.click()
151+
return
152+
except Exception:
153+
# If anything goes wrong, just proceed; the overlay handler will try again.
154+
pass
155+
156+
def _get_play_button(self):
157+
"""Return a clickable play button element if available."""
158+
159+
try:
160+
return self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".ytp-play-button")))
161+
except TimeoutException:
162+
buttons = self.driver.find_elements(By.CSS_SELECTOR, ".ytp-play-button")
163+
for btn in buttons:
164+
if btn.is_displayed() and btn.is_enabled():
165+
return btn
166+
return None
167+
168+
def _click_play_button(self):
169+
"""Try clicking the play button with retries to avoid stale references."""
170+
171+
for _ in range(3):
172+
btn = self._get_play_button()
173+
if not btn:
174+
continue
175+
try:
176+
self.driver.execute_script(
177+
"arguments[0].scrollIntoView({block: 'center', inline: 'center'});", btn
178+
)
179+
self.driver.execute_script("arguments[0].click();", btn)
180+
return True
181+
except (StaleElementReferenceException, ElementClickInterceptedException):
182+
self._dismiss_overlays()
183+
continue
184+
return False
64185

65186
def clear_cache(self):
66187
"""Clear the cache"""
@@ -70,15 +191,52 @@ def clear_cache(self):
70191
def refresh(self):
71192
"""Refresh the current tab"""
72193

194+
# Reload the intended video instead of refreshing whatever autoplay selected
73195
self.driver.implicitly_wait(5)
74-
self.driver.refresh()
196+
self.driver.get(self.website)
197+
self._accept_cookies()
75198

76199
def switch_tab(self, tab):
77200
"""Switch to the specified tab"""
78201

79-
self.driver.switch_to.window(self.driver.window_handles[tab])
202+
try:
203+
self.driver.switch_to.window(self.driver.window_handles[tab])
204+
except (InvalidSessionIdException, NoSuchWindowException, IndexError):
205+
# Browser/tab closed or session lost; try to rebuild session so script does not exit
206+
self._restart_session()
207+
try:
208+
self.driver.switch_to.window(self.driver.window_handles[tab])
209+
except Exception:
210+
pass
80211

81212
def new_tab(self):
82213
"""Open a blank new tab"""
83214

84215
self.driver.execute_script("window.open('about:blank');")
216+
217+
def _restart_session(self):
218+
"""Rebuild the browser session if it was lost."""
219+
220+
try:
221+
self.driver.quit()
222+
except Exception:
223+
pass
224+
225+
self.driver = get_driver(self.browser)
226+
self.wait = WebDriverWait(self.driver, 20)
227+
228+
# Reopen tabs and load the target website in each
229+
handles_needed = max(self.tab_amount, 1)
230+
# Open first tab and load
231+
self.driver.get(self.website)
232+
self._accept_cookies()
233+
234+
# Create remaining tabs
235+
for _ in range(handles_needed - 1):
236+
self.new_tab()
237+
238+
# Load website in every tab
239+
for handle in self.driver.window_handles:
240+
self.driver.switch_to.window(handle)
241+
self.driver.get(self.website)
242+
self._accept_cookies()

main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def get_config():
1818
def init_tabs(website, tab_amount):
1919
"""Opens tabs according to tab amount set in config.json"""
2020

21-
for _ in range(tab_amount):
21+
# First tab already exists; only open the remaining tabs
22+
for _ in range(max(tab_amount - 1, 0)):
2223
website.new_tab()
2324

2425
def open_links(website, tab_amount):
@@ -51,7 +52,7 @@ def main():
5152
print('Initilization')
5253
config = get_config()
5354

54-
website = driver.Bot(config['website'], config['browser'])
55+
website = driver.Bot(config['website'], config['browser'], config['tab_amount'])
5556

5657
print('Opening new tabs')
5758
init_tabs(website, config['tab_amount'])

0 commit comments

Comments
 (0)