Skip to content

Commit 56c2360

Browse files
committed
Add Selenium project code
1 parent 754c56e commit 56c2360

File tree

13 files changed

+318
-0
lines changed

13 files changed

+318
-0
lines changed

python-selenium/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.8.
16+
17+
## Run the Bandcamp Discover Player
18+
19+
To run the music placer, navigate to the `src/` folder, then execute the module from your command-line:
20+
21+
```sh
22+
(venv) $ cd src/
23+
(venv) $ python -m bandcamp
24+
```
25+
26+
You'll see a text-based user interface that allows you to interact with the music player:
27+
28+
```
29+
Type: [play <track number>], [tracks], [more], [exit]
30+
>
31+
```
32+
33+
Type one of the available commands to interact with Bandcamp's Discover section through your headless browser. Listen to songs with `play`, list available tracks with `tracks`, and load more songs using `more`. You can exit the music player by typing `exit`.
34+
35+
## About the Authors
36+
37+
Martin Breuss - Email: [email protected]
38+
Bartosz Zaczyński - Email: [email protected]
39+
40+
## License
41+
42+
Distributed under the MIT license. See ``LICENSE`` for more information.

python-selenium/pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "bandcamp_player"
7+
version = "0.1.0"
8+
requires-python = ">=3.8"
9+
description = "A web player for Bandcamp using Selenium"
10+
authors = [
11+
{ name = "Martin Breuss", email = "[email protected]" },
12+
{ name = "Bartosz Zaczyński", email = "[email protected]" },
13+
]
14+
dependencies = [
15+
"selenium",
16+
]
17+
[project.scripts]
18+
bandcamp-player = "bandcamp.__main__:main"
19+
20+
[tool.setuptools.packages.find]
21+
where = ["src"]

python-selenium/requirements.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
appdirs==1.4.4
2+
attrs==24.2.0
3+
certifi==2024.7.4
4+
h11==0.14.0
5+
idna==3.7
6+
jedi==0.19.1
7+
outcome==1.3.0.post0
8+
parso==0.8.4
9+
prompt_toolkit==3.0.47
10+
ptpython==3.0.29
11+
Pygments==2.18.0
12+
PySocks==1.7.1
13+
selenium==4.23.1
14+
sniffio==1.3.1
15+
sortedcontainers==2.4.0
16+
trio==0.26.1
17+
trio-websocket==0.11.1
18+
typing_extensions==4.12.2
19+
urllib3==2.2.2
20+
wcwidth==0.2.13
21+
websocket-client==1.8.0
22+
wsproto==1.2.0

python-selenium/src/bandcamp/__init__.py

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from bandcamp.app.tui import TUI
2+
3+
4+
def main():
5+
"""Provides the main entry point for the app."""
6+
tui = TUI()
7+
tui.interact()
8+
9+
10+
if __name__ == "__main__":
11+
main()

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

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from selenium.webdriver import Firefox
2+
from selenium.webdriver.firefox.options import Options
3+
4+
from bandcamp.web.element import TrackElement
5+
from bandcamp.web.page import HomePage
6+
7+
BANDCAMP_FRONTPAGE = "https://bandcamp.com/"
8+
9+
10+
class Player:
11+
"""Plays tracks from Bandcamp's Discover section."""
12+
13+
def __init__(self) -> None:
14+
self._driver = self._set_up_driver()
15+
self.home = HomePage(self._driver)
16+
self.discover = self.home.discover_tracklist
17+
self._current_track = TrackElement(
18+
self.home.discover_tracklist.available_tracks[0], self._driver
19+
)
20+
21+
def __enter__(self):
22+
return self
23+
24+
def __exit__(self, exc_type, exc_value, exc_tb):
25+
"""Closes the headless browser."""
26+
self._driver.close()
27+
28+
def play(self, track_number=None):
29+
"""Plays the first track, or one of the available numbered tracks."""
30+
if track_number:
31+
self._current_track = TrackElement(
32+
self.home.discover_tracklist.available_tracks[
33+
track_number - 1
34+
],
35+
self._driver,
36+
)
37+
self._current_track.play()
38+
39+
def _set_up_driver(self):
40+
"""Creates a headless browser pointing to Bandcamp."""
41+
options = Options()
42+
options.add_argument("--headless")
43+
browser = Firefox(options=options)
44+
browser.get(BANDCAMP_FRONTPAGE)
45+
return browser
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from bandcamp.app.player import Player
2+
3+
4+
class TUI:
5+
"""Provides a text-based user interface for a Bandcamp music player."""
6+
7+
COLUMN_WIDTH = CW = 30
8+
9+
def interact(self):
10+
"""Controls the player through user interactions."""
11+
with Player() as player:
12+
while True:
13+
print(
14+
"\nType: [play <track number>], [tracks], [more], [exit]"
15+
)
16+
command = input("> ").strip().lower()
17+
18+
if command.startswith("play"):
19+
try:
20+
track_number = int(command.split()[1])
21+
self.play(player, track_number)
22+
except IndexError: # Play first track.
23+
self.play(player)
24+
except ValueError:
25+
print("Please provide a valid track number.")
26+
elif command == "tracks":
27+
self.tracks(player)
28+
elif command == "more":
29+
player.discover.load_more()
30+
self.tracks(player)
31+
elif command == "exit":
32+
print("Exiting the player...")
33+
break
34+
else:
35+
print("Unknown command. Try again.")
36+
37+
def play(self, player, track_number=None):
38+
"""Plays a track and shows info about the track."""
39+
player.play(track_number)
40+
print(player._current_track._get_track_info())
41+
42+
def tracks(self, player):
43+
"""Displays information about the currently playable tracks."""
44+
header = (
45+
f"{'#':<5} {'Album':<{self.CW}} "
46+
f"{'Artist':<{self.CW}} "
47+
f"{'Genre':<{self.CW}}"
48+
)
49+
print(header)
50+
print("-" * 100)
51+
for track_number, track in enumerate(
52+
player.discover.available_tracks, start=1
53+
):
54+
album, artist, *genre = track.text.split("\n")
55+
album = self._truncate(album, self.CW)
56+
artist = self._truncate(artist, self.CW)
57+
genre = self._truncate(genre[0], self.CW) if genre else ""
58+
print(
59+
f"{track_number:<5} {album:<{self.CW}} "
60+
f"{artist:<{self.CW}} {genre:<{self.CW}}"
61+
)
62+
63+
@staticmethod
64+
def _truncate(text, width):
65+
"""Truncates track information."""
66+
return text[: width - 3] + "..." if len(text) > width else text

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

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

0 commit comments

Comments
 (0)