Skip to content

Commit ae32268

Browse files
authored
Merge pull request #596 from realpython/python-selenium
Add slimmed-down Selenium project code
2 parents e1e1066 + d813234 commit ae32268

File tree

18 files changed

+738
-0
lines changed

18 files changed

+738
-0
lines changed

python-selenium/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Modern Web Automation With Python and Selenium
2+
3+
This repository contains the module `bandcamp`, which is the sample app built in the Real Python tutorial [Modern Web Automation With Python and Selenium](https://realpython.com/modern-web-automation-with-python-and-selenium/).
4+
5+
## Installation and Setup
6+
7+
Create and activate a [Python virtual environment](https://realpython.com/python-virtual-environments-a-primer/).
8+
9+
Then, install the requirements:
10+
11+
```sh
12+
(venv) $ python -m pip install -r requirements.txt
13+
```
14+
15+
The only direct dependency for this project is [Selenium](https://selenium-python.readthedocs.io/). You should use a Python version of at least 3.10, which is necessary to support [structural pattern matching](https://realpython.com/structural-pattern-matching/).
16+
17+
You'll need a [Firefox Selenium driver](https://selenium-python.readthedocs.io/installation.html#drivers) called `geckodriver` to run the project as-is. Make sure to [download and install](https://github.com/mozilla/geckodriver/releases) it before running the project.
18+
19+
## Run the Bandcamp Discover Player
20+
21+
To run the music player, install the package, then use the entry-point command from your command line:
22+
23+
```sh
24+
(venv) $ python -m pip install .
25+
(venv) $ bandcamp-player
26+
```
27+
28+
You'll see a text-based user interface that allows you to interact with the music player:
29+
30+
```
31+
Type: play [<track_number>] | pause | tracks | more | exit
32+
>
33+
```
34+
35+
Type one of the available commands to interact with Bandcamp's Discover section through your headless browser. Listen to songs with `play`, pause the current song with `pause`, and restart it with `play`. List available tracks with `tracks`, and load more songs using `more`. You can exit the music player by typing `exit`.
36+
37+
## Troubleshooting
38+
39+
If the music player seems to hang when you run the script, confirm whether you've correctly set up your WebDriver based on the following points.
40+
41+
### Version Compatibility
42+
43+
Confirm that your browser and corresponding WebDriver are in sync. If you followed the previous suggestion, then you should be using Firefox and geckodriver. If that doesn't work for some reason, then you may need to switch your browser _and_ WebDriver.
44+
45+
For example, if you're using Chrome, then you need to install ChromeDriver and it must match your Chrome version. Otherwise, you may run into errors like `SessionNotCreatedException`.
46+
For more details, refer to the official [ChromeDriver documentation](https://sites.google.com/chromium.org/driver/) or [geckodriver releases](https://github.com/mozilla/geckodriver/releases).
47+
48+
### Driver Installation and Path Issues
49+
50+
Once you've confirmed that your browser and driver match, make sure that the WebDriver executable is properly installed:
51+
52+
- **Path Configuration:** The driver must be in your system's PATH, or you need to specify its full path in your code.
53+
- **Permissions:** Ensure the driver file has the necessary execution permissions.
54+
55+
If you're still running into issues executing the script, then consult the [Selenium Documentation](https://www.selenium.dev/documentation/) for additional troubleshooting tips or leave a comment in the tutorial.
56+
57+
## About the Authors
58+
59+
Martin Breuss - Email: [email protected]
60+
Bartosz Zaczyński - Email: [email protected]
61+
62+
## License
63+
64+
Distributed under the MIT license.

python-selenium/pyproject.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "pycamp"
7+
version = "0.1.1"
8+
requires-python = ">=3.10"
9+
description = "A CLI music player for Bandcamp's Discover page using Python and Selenium"
10+
license = "MIT"
11+
classifiers = [
12+
"Programming Language :: Python :: 3",
13+
"Operating System :: OS Independent"
14+
]
15+
authors = [
16+
{ name = "Martin Breuss", email = "[email protected]" },
17+
{ name = "Bartosz Zaczyński", email = "[email protected]" },
18+
]
19+
dependencies = [
20+
"selenium",
21+
]
22+
[project.scripts]
23+
discover = "bandcamp.__main__:main"
24+
pycamp = "bandcamp.__main__:main"

python-selenium/requirements.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
attrs==25.2.0
2+
certifi==2025.1.31
3+
h11==0.14.0
4+
idna==3.10
5+
outcome==1.3.0.post0
6+
pysocks==1.7.1
7+
selenium==4.29.0
8+
sniffio==1.3.1
9+
sortedcontainers==2.4.0
10+
trio==0.29.0
11+
trio-websocket==0.12.2
12+
typing-extensions==4.12.2
13+
urllib3==2.3.0
14+
websocket-client==1.8.0
15+
wsproto==1.2.0

python-selenium/src/bandcamp/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from bandcamp.app.tui import interact
2+
3+
4+
def main():
5+
"""Provide the main entry point for the app."""
6+
interact()

python-selenium/src/bandcamp/app/__init__.py

Whitespace-only changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from selenium.webdriver import Firefox
2+
from selenium.webdriver.firefox.options import Options
3+
4+
from bandcamp.web.pages import DiscoverPage
5+
6+
BANDCAMP_DISCOVER_URL = "https://bandcamp.com/discover/"
7+
8+
9+
class Player:
10+
"""Play tracks from Bandcamp's Discover page."""
11+
12+
def __init__(self) -> None:
13+
self._driver = self._set_up_driver()
14+
self.page = DiscoverPage(self._driver)
15+
self.tracklist = self.page.discover_tracklist
16+
self._current_track = self.tracklist.available_tracks[0]
17+
18+
def __enter__(self):
19+
return self
20+
21+
def __exit__(self, exc_type, exc_value, exc_tb):
22+
"""Close the headless browser."""
23+
self._driver.quit()
24+
25+
def play(self, track_number=None):
26+
"""Play the first track, or one of the available numbered tracks."""
27+
if track_number:
28+
self._current_track = self.tracklist.available_tracks[
29+
track_number - 1
30+
]
31+
self._current_track.play()
32+
33+
def pause(self):
34+
"""Pause the current track."""
35+
self._current_track.pause()
36+
37+
def _set_up_driver(self):
38+
"""Create a headless browser pointing to Bandcamp."""
39+
options = Options()
40+
options.add_argument("--headless")
41+
browser = Firefox(options=options)
42+
browser.get(BANDCAMP_DISCOVER_URL)
43+
return browser
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from bandcamp.app.player import Player
2+
3+
COLUMN_WIDTH = CW = 30
4+
MAX_TRACKS = 100 # Allows to load more tracks once.
5+
6+
7+
def interact():
8+
"""Control the player through user interactions."""
9+
with Player() as player:
10+
while True:
11+
print(
12+
"\nType: play [<track number>] | pause | tracks | more | exit"
13+
)
14+
match input("> ").strip().lower().split():
15+
case ["play"]:
16+
play(player)
17+
case ["play", track]:
18+
try:
19+
track_number = int(track)
20+
play(player, track_number)
21+
except ValueError:
22+
print("Please provide a valid track number.")
23+
case ["pause"]:
24+
pause(player)
25+
case ["tracks"]:
26+
display_tracks(player)
27+
case ["more"] if (
28+
len(player.tracklist.available_tracks) >= MAX_TRACKS
29+
):
30+
print(
31+
"Can't load more tracks. Pick one from the track list."
32+
)
33+
case ["more"]:
34+
player.tracklist.load_more()
35+
display_tracks(player)
36+
case ["exit"]:
37+
print("Exiting the player...")
38+
break
39+
case _:
40+
print("Unknown command. Try again.")
41+
42+
43+
def play(player, track_number=None):
44+
"""Play a track and show info about the track."""
45+
try:
46+
player.play(track_number)
47+
print(player._current_track._get_track_info())
48+
except IndexError:
49+
print(
50+
"Please provide a valid track number. "
51+
"You can list available tracks with `tracks`."
52+
)
53+
54+
55+
def pause(player):
56+
"""Pause the current track."""
57+
player.pause()
58+
59+
60+
def display_tracks(player):
61+
"""Display information about the currently playable tracks."""
62+
header = f"{'#':<5} {'Album':<{CW}} {'Artist':<{CW}} {'Genre':<{CW}}"
63+
print(header)
64+
print("-" * 80)
65+
for track_number, track_element in enumerate(
66+
player.tracklist.available_tracks, start=1
67+
):
68+
track = track_element._get_track_info()
69+
album = _truncate(track.album, CW)
70+
artist = _truncate(track.artist, CW)
71+
genre = _truncate(track.genre, CW)
72+
print(f"{track_number:<5} {album:<{CW}} {artist:<{CW}} {genre:<{CW}}")
73+
74+
75+
def _truncate(text, width):
76+
"""Truncate track information."""
77+
return text[: width - 3] + "..." if len(text) > width else text

python-selenium/src/bandcamp/web/__init__.py

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import dataclass
2+
from pprint import pformat
3+
4+
from selenium.webdriver.remote.webdriver import WebDriver
5+
from selenium.webdriver.remote.webelement import WebElement
6+
from selenium.webdriver.support.wait import WebDriverWait
7+
8+
MAX_WAIT_SECONDS = 10.0
9+
DEFAULT_WINDOW_SIZE = (1920, 3000)
10+
11+
12+
@dataclass
13+
class Track:
14+
album: str
15+
artist: str
16+
genre: str
17+
url: str
18+
19+
def __str__(self):
20+
return pformat(self)
21+
22+
23+
class WebPage:
24+
def __init__(self, driver: WebDriver) -> None:
25+
self._driver = driver
26+
self._driver.set_window_size(*DEFAULT_WINDOW_SIZE)
27+
self._driver.implicitly_wait(5)
28+
self._wait = WebDriverWait(driver, MAX_WAIT_SECONDS)
29+
30+
31+
class WebComponent(WebPage):
32+
def __init__(self, parent: WebElement, driver: WebDriver) -> None:
33+
super().__init__(driver)
34+
self._parent = parent

0 commit comments

Comments
 (0)