Skip to content

Add Code for Selenium Web Automation Tutorial Update #583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions selenium-web-automation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Modern Web Automation With Python and Selenium

This folder contains the code for the Real Python tutorial on [Modern Web Automation With Python and Selenium](https://realpython.com/modern-web-automation-with-python-and-selenium/).

## Setup

Create and activate a virtual environment, then install the dependencies:

```sh
(venv) $ python -m pip install .
```

## Usage

To start streaming music from BandCamp's _Discover_ section, you can execute the script:

```sh
(venv) $ bandcamp-player
```

## Author

Martin Breuss – [email protected]

## License

This project is distributed under the MIT license.
24 changes: 24 additions & 0 deletions selenium-web-automation/bandcamp_player/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "bandcamp_player"
version = "0.1.0"
description = "A web player for Bandcamp using Selenium"
authors = [
{ name = "Martin Breuss", email = "[email protected]" },
{ name = "Bartosz Zaczyński", email = "[email protected]" },
]
dependencies = [
"selenium",
"textual",
]
[project.scripts]
bandcamp-player = "bandcamp.__main__:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"*" = ["*.css"]
Empty file.
20 changes: 20 additions & 0 deletions selenium-web-automation/bandcamp_player/src/bandcamp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from bandcamp.tui.app import BandcampApp
from bandcamp.workers.messages import Message
from bandcamp.workers.storage import StorageWorker
from bandcamp.workers.web import WebWorker


def main() -> None:
storage_worker = StorageWorker()
storage_worker.start()
web_worker = WebWorker()
web_worker.start()
try:
BandcampApp(storage_worker, web_worker).run()
finally:
storage_worker.inbox.put(Message.GRACEFUL_STOP)
web_worker.inbox.put(Message.GRACEFUL_STOP)


if __name__ == "__main__":
main()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import sqlite3
from dataclasses import astuple
from pathlib import Path

from bandcamp.storage.models import Track

DATABASE_PATH = Path.home() / "bandcamp.db"
SQL_CREATE = """\
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
title TEXT,
artist TEXT,
artist_url TEXT,
album TEXT,
album_url TEXT,
genre TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
SQL_INSERT = """\
INSERT INTO history (id, title, artist, artist_url, album, album_url, genre)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
SQL_SELECT_ALL = "SELECT * FROM history"
SQL_SELECT_ONE = "SELECT id FROM history WHERE id=?"


class Database:
def __init__(self):
self.connection = sqlite3.connect(DATABASE_PATH)
self.cursor = self.connection.cursor()
self.create_table()

def create_table(self):
self.cursor.execute(SQL_CREATE)
self.connection.commit()

def persist(self, track: Track):
self.cursor.execute(SQL_SELECT_ONE, (track.id,))
if not self.cursor.fetchone():
self.cursor.execute(SQL_INSERT, (track.id, *astuple(track)))
self.connection.commit()

def find_all(self):
self.cursor.execute(SQL_SELECT_ALL)
return [Track(*row[1:-1]) for row in self.cursor.fetchall()]

def close(self):
self.connection.close()
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import hashlib
from dataclasses import astuple, dataclass


@dataclass(frozen=True)
class Track:
title: str
artist: str
artist_url: str
album: str | None = None
album_url: str | None = None
genre: str | None = None

@property
def id(self) -> str:
data = "".join([str(x) for x in astuple(self)]).encode("utf-8")
return hashlib.md5(data).hexdigest()
Empty file.
33 changes: 33 additions & 0 deletions selenium-web-automation/bandcamp_player/src/bandcamp/tui/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.horizontal {
layout: horizontal;
margin: 1;
}

.hidden {
display: none;
}

Playlist {
height: 80%;
}

DataTable {
margin-right: 1;
height: 100%;
}

#current_track {
content-align: center middle;
height: 3;
margin-left: 2;
}

#pager-buttons {
width: 20;
}

#pager-buttons Button {
width: 20;
height: 3;
margin-bottom: 1;
}
25 changes: 25 additions & 0 deletions selenium-web-automation/bandcamp_player/src/bandcamp/tui/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from textual.app import App
from textual.widgets import Footer, Header

from bandcamp.tui.state import AppStateMixin
from bandcamp.tui.widgets import Player, Playlist


class BandcampApp(AppStateMixin, App):
CSS_PATH = "app.css"
TITLE = "Bandcamp Player"
BINDINGS = [
("q", "quit", "Quit"),
("space", "toggle_play", "Play/Pause"),
]

def __init__(self, storage_worker, web_worker):
super().__init__()
self.storage_worker = storage_worker
self.web_worker = web_worker

def compose(self):
yield Header()
yield Footer()
yield Player(classes="horizontal")
yield Playlist(classes="horizontal")
70 changes: 70 additions & 0 deletions selenium-web-automation/bandcamp_player/src/bandcamp/tui/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from rich.text import Text
from textual.widgets import DataTable, Label

from bandcamp.workers.messages import Message


class AppStateMixin:
def action_toggle_play(self):
if self.query_one("#play").has_class("hidden"):
self.query_one("#pause").add_class("hidden")
self.query_one("#play").remove_class("hidden")
self.call_after_refresh(self.pause)
else:
self.query_one("#play").add_class("hidden")
self.query_one("#pause").remove_class("hidden")
self.call_after_refresh(self.play)
self.call_after_refresh(self.persist_current_track)

def play(self):
self.web_worker.inbox.put(Message.PLAY)

def pause(self):
self.web_worker.inbox.put(Message.PAUSE)

def persist_current_track(self):
track = self.web_worker.request(Message.CURRENT_TRACK)
self.storage_worker.inbox.put(track)


class PlayerStateMixin:
def on_mount(self):
label = self.query_one("#current_track", Label)
track = self.app.web_worker.request(Message.FIRST_TRACK)
label.update(track.title)


class PlaylistStateMixin:
def on_mount(self):
self.update_table()

def move_next(self):
self.app.web_worker.request(Message.NEXT_PAGE)
self.update_table()

def move_previous(self):
self.app.web_worker.request(Message.PREVIOUS_PAGE)
self.update_table()

def play_row(self, index: int):
title = self.app.web_worker.request((Message.PLAY_TRACK, index))
self.app.query_one("#current_track", Label).update(title)
self.app.query_one("#play").add_class("hidden")
self.app.query_one("#pause").remove_class("hidden")
self.call_after_refresh(self.app.persist_current_track)

def update_table(self):
page_number, visible_tracks = self.app.web_worker.request(Message.PAGE)
offset = 8 * (page_number - 1)
rows = [
[
Text(f"{offset + i}.", justify="right"),
track.title,
track.artist,
track.genre,
]
for i, track in enumerate(visible_tracks, 1)
]
table = self.query_one("#table", DataTable)
table.clear()
table.add_rows(rows)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from textual import on
from textual.widgets import Button, DataTable, Label, Static

from bandcamp.tui.state import PlayerStateMixin, PlaylistStateMixin


class Player(PlayerStateMixin, Static):
def compose(self):
yield Button("Play", variant="success", id="play")
yield Button("Pause", variant="error", id="pause", classes="hidden")
yield Label("N/A", id="current_track")

@on(Button.Pressed, "#play")
def on_play_click(self):
self.call_after_refresh(self.app.action_toggle_play)

@on(Button.Pressed, "#pause")
def on_pause_click(self):
self.call_after_refresh(self.app.action_toggle_play)


class Playlist(PlaylistStateMixin, Static):
def compose(self):
table = DataTable(id="table")
table.cursor_type = "row"
table.add_columns("Track", "Title", "Artist", "Genre")
table.move_cursor(row=0)
table.focus()
yield table
with Static(id="pager-buttons"):
yield Button("Next Page ›", id="next")
yield Button("‹ Previous Page", id="previous")

@on(Button.Pressed, "#next")
def on_next_click(self):
self.call_after_refresh(self.move_next)

@on(Button.Pressed, "#previous")
def on_previous_click(self):
self.call_after_refresh(self.move_previous)

@on(DataTable.RowSelected, "#table")
def on_row_selected(self, event):
self.call_after_refresh(self.play_row, event.cursor_row)
Empty file.
Loading
Loading