{"id":26927655,"url":"https://github.com/lmmx/beeb","last_synced_at":"2026-02-28T08:36:14.266Z","repository":{"id":57414482,"uuid":"347124371","full_name":"lmmx/beeb","owner":"lmmx","description":"A modern interface to the BBC Sounds radio catalogue","archived":false,"fork":false,"pushed_at":"2021-04-01T21:41:47.000Z","size":11580,"stargazers_count":5,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-03T05:27:50.630Z","etag":null,"topics":["bbc-iplayer","radio"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lmmx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-03-12T16:12:34.000Z","updated_at":"2024-03-02T10:56:29.000Z","dependencies_parsed_at":"2022-09-02T14:51:36.473Z","dependency_job_id":null,"html_url":"https://github.com/lmmx/beeb","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/lmmx/beeb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lmmx%2Fbeeb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lmmx%2Fbeeb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lmmx%2Fbeeb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lmmx%2Fbeeb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lmmx","download_url":"https://codeload.github.com/lmmx/beeb/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lmmx%2Fbeeb/sbom","scorecard":{"id":596231,"data":{"date":"2025-08-11","repo":{"name":"github.com/lmmx/beeb","commit":"26e5dc3c4ff9590d3518c7015423bbc795606f6d"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/tests.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/lmmx/beeb/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/lmmx/beeb/tests.yml/master?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/tests.yml:41","Warn: pipCommand not pinned by hash: .github/workflows/tests.yml:42","Warn: pipCommand not pinned by hash: .github/workflows/tests.yml:46","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   3 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-20T23:12:24.771Z","repository_id":57414482,"created_at":"2025-08-20T23:12:24.772Z","updated_at":"2025-08-20T23:12:24.772Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29928984,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T19:37:42.220Z","status":"online","status_checked_at":"2026-02-28T02:00:07.010Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bbc-iplayer","radio"],"created_at":"2025-04-02T04:17:10.904Z","updated_at":"2026-02-28T08:36:14.246Z","avatar_url":"https://github.com/lmmx.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# beeb\n\n![](https://raw.githubusercontent.com/lmmx/beeb/master/assets/beeb_logo.png)\n\nA modern interface to the BBC Sounds radio catalogue.\n\n## Motivation\n\n_beeb_ is a light alternative to\n[`get_iplayer`](https://github.com/get-iplayer/get_iplayer/)\n(10,000 lines of Perl)\nand\n[`youtube-dl`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/bbc.py)\n(750+ compatible sites).\n\nThese libraries aim to be deep and broad, `beeb` just aims to be specific and quick,\nhandling common use cases (scheduling and download links) clearly, in nicely\nobject-oriented Python.\n\n## Requirements\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here to show requirements\u003c/summary\u003e\n\n\u003cp\u003e\n\n- BeautifulSoup\n- HTTPX\n  - A `requests`-like API with async and HTTP/2 support\n- aiostream, aiofiles\n  - Asynchronous requests to speed up channel listings retrieval and stream downloads\n- tqdm\n  - Gives the option to show progress when multiprocessing channel listings\n- `more_itertools`\n- dateutil\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n## Schedule handling\n\nA `ChannelSchedule` stores a single day's listings, for a single channel.\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for basic info on ChannelSchedule\u003c/summary\u003e\n\n\u003cp\u003e\n\n- National, local, regional channels can be selected by ID or short name\n- The schedule with today's date is loaded by default\n\nTo load today's schedule for BBC R4:\n\n```py\nfrom beeb.nav import ChannelSchedule\nChannelSchedule.from_channel_name(\"r4\")\n```\n⇣\n```\nChannelSchedule for BBC Radio 4 on 2021-03-17\n```\n\nThese ChannelSchedule objects can be used to find programmes:\n\n```py\n\u003e\u003e\u003e from beeb.nav import ChannelSchedule\n\u003e\u003e\u003e s = ChannelSchedule.from_channel_name(\"r4\")\n\u003e\u003e\u003e s.get_broadcast_by_title(\"Today\", pid_only=True)\n'm000t476'\n\u003e\u003e\u003e s.get_broadcast_by_title(\"Midnight News\")\n00:00 on 17/03/2021 — Midnight News\n\u003e\u003e\u003e for b in s.get_broadcast_by_title(\"Shipping Forecast\", multi=True): b\n00:48 on 17/03/2021 — Shipping Forecast\n05:20 on 17/03/2021 — Shipping Forecast\n12:03 on 17/03/2021 — Shipping Forecast\n```\n\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for more complex ChannelSchedule examples\u003c/summary\u003e\n\n\u003cp\u003e\n\n```py\n\u003e\u003e\u003e for b in s.get_broadcast_by_title(r\".*\\bNews\\b\", regex=True, multi=True): b\n... \n00:00 on 17/03/2021 — Midnight News\n05:30 on 17/03/2021 — News Briefing\n12:00 on 17/03/2021 — News Summary\n18:00 on 17/03/2021 — Six O'Clock News\n\u003e\u003e\u003e for b in s.get_broadcast_by_title(r\".*\\bnews\\b\", multi=True,\n... case_insensitive=True, regex=True, synopsis=True): print(b)\n... \n00:00 on 17/03/2021 — Midnight News\n05:30 on 17/03/2021 — News Briefing\n06:00 on 17/03/2021 — Today\n12:00 on 17/03/2021 — News Summary\n13:00 on 17/03/2021 — World at One\n17:00 on 17/03/2021 — PM\n18:00 on 17/03/2021 — Six O'Clock News\n20:00 on 17/03/2021 — Moral Maze\n22:00 on 17/03/2021 — The World Tonight\n23:30 on 17/03/2021 — Today in Parliament\n\u003e\u003e\u003e for b in s.get_broadcast_by_title(\n... r\".*\\b(pandemic|virus|coronavirus|Covid|vaccines?|vaccinations?|health|healthcare|NHS)\\b\",\n... multi=True, case_insensitive=True, regex=True, synopsis=True): print(b)\n... \n10:00 on 17/03/2021 — Woman's Hour\n15:00 on 17/03/2021 — Money Box\n15:30 on 17/03/2021 — Inside Health\n```\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n`ChannelListings` are a collection of `ChannelSchedule` objects over a\ngiven time period (from up to 30 days ago).\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for basic info on ChannelListings\u003c/summary\u003e\n\n\u003cp\u003e\n\nThe schedules are loaded asynchronously and then their HTML is parsed on all CPU cores (fast!)\n\nNo interface is currently implemented for multiple channels (as I don't particularly need it), but\n`beeb.nav.ChannelPicker` gives all available channels if you wanted to iterate over them.\n\n```py\n\u003e\u003e\u003e from beeb.nav import ChannelListings\n\u003e\u003e\u003e ChannelListings.from_channel_name(\"r4\")\nChannelListings for BBC Radio 4 from 2021-02-17 to 2021-03-18 (30 days)\n```\n\nThe schedules are stored as a chronological list in the `ChannelListings.schedules` attribute\n\n```py\n\u003e\u003e\u003e from beeb.nav import ChannelListings\n\u003e\u003e\u003e l = ChannelListings.from_channel_name(\"r4\")\nChannelListings for BBC Radio 4 from 2021-02-17 to 2021-03-18 (30 days)\n\u003e\u003e\u003e l.schedules[0]\nChannelSchedule for BBC Radio 4 on 2021-02-17\n```\n\nChannelListings follows the same interface as ChannelSchedule (they share a commonly bound method,\nto avoid too much parameter passing).\n\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for examples of ChannelListings queries\u003c/summary\u003e\n\n\u003cp\u003e\n\n\n- There were 26 'Today' episodes aired on BBC R4 in the last 30 days (not aired on Sundays):\n\n```py\n\u003e\u003e\u003e for i, b in enumerate(l.get_broadcast_by_title(\"Today\", multi=True)):\n...     print(f\"{i:2}) {b}\")\n... \n 0) 06:00 on Wed 17/02/2021 — Today\n 1) 06:00 on Thu 18/02/2021 — Today\n 2) 06:00 on Fri 19/02/2021 — Today\n 3) 07:00 on Sat 20/02/2021 — Today\n 4) 06:00 on Mon 22/02/2021 — Today\n 5) 06:00 on Tue 23/02/2021 — Today\n 6) 06:00 on Wed 24/02/2021 — Today\n 7) 06:00 on Thu 25/02/2021 — Today\n 8) 06:00 on Fri 26/02/2021 — Today\n 9) 07:00 on Sat 27/02/2021 — Today\n10) 06:00 on Mon 01/03/2021 — Today\n11) 06:00 on Tue 02/03/2021 — Today\n12) 06:00 on Wed 03/03/2021 — Today\n13) 06:00 on Thu 04/03/2021 — Today\n14) 06:00 on Fri 05/03/2021 — Today\n15) 07:00 on Sat 06/03/2021 — Today\n16) 06:00 on Mon 08/03/2021 — Today\n17) 06:00 on Tue 09/03/2021 — Today\n18) 06:00 on Wed 10/03/2021 — Today\n19) 06:00 on Thu 11/03/2021 — Today\n20) 06:00 on Fri 12/03/2021 — Today\n21) 07:00 on Sat 13/03/2021 — Today\n22) 06:00 on Mon 15/03/2021 — Today\n23) 06:00 on Tue 16/03/2021 — Today\n24) 06:00 on Wed 17/03/2021 — Today\n25) 06:00 on Thu 18/03/2021 — Today\n```\n\n- Here's a query of all programmes which mention vaccin(es,ations,inologists) in their\n  title/subtitle/synopsis:\n\n```py\n\u003e\u003e\u003e for b in l.get_broadcast_by_title(r\".*\\b(vaccin.+?)\\b\", multi=True, case_insensitive=True,\n... regex=True, synopsis=True): print(b)\n... \n15:30 on Wed 17/02/2021 — Inside Health\n18:00 on Wed 17/02/2021 — Six O'Clock News\n11:30 on Mon 22/02/2021 — How to Vaccinate the World\n14:00 on Sat 27/02/2021 — Any Answers?\n07:10 on Sun 28/02/2021 — Sunday\n11:30 on Mon 01/03/2021 — How to Vaccinate the World\n20:00 on Wed 03/03/2021 — Moral Maze\n22:15 on Sat 06/03/2021 — Moral Maze\n11:30 on Mon 08/03/2021 — How to Vaccinate the World\n11:30 on Mon 15/03/2021 — How to Vaccinate the World\n18:00 on Mon 15/03/2021 — Six O'Clock News\n22:00 on Mon 15/03/2021 — The World Tonight\n21:00 on Tue 16/03/2021 — Inside Health\n15:30 on Wed 17/03/2021 — Inside Health\n18:00 on Wed 17/03/2021 — Six O'Clock News\n```\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\nFrom the ChannelListings you can build `ProgrammeCatalogue` dictionaries,\nby inspecting the parent programme IDs of each of the broadcasts in the schedules\nwithin the listings. Catalogues for multiple stations can be organised in a `ProgrammeGuide`.\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for ProgrammeCatalogue examples\u003c/summary\u003e\n\n\u003cp\u003e\n\nTo obtain an up-to-date `ProgrammeCatalogue` with just programme PIDs and titles:\n\n```py\nbeeb.api.get_programme_dict(\"r4\", n_days=1)\n```\n⇣\n```py\n{'b00cs19l': 'Midnight News',\n 'b006qfvv': 'Shipping Forecast',\n 'b006s54y': 'Selection of BBC World Service Programmes',\n 'b007rhyn': 'News Briefing',\n 'b006qmpj': 'Prayer for the Day',\n 'b006qj8q': 'Farming Today',\n 'b01s6xyk': 'Tweet of the Day',\n 'b006qj9z': 'Today',\n 'b09zgd6y': 'Chinese Characters',\n 'b007qlvb': \"Woman's Hour\",\n 'm0002rjm': \"Alexei Sayle's The Absence of Normal\",\n 'b04fc120': 'News Summary',\n 'b006qps9': 'You and Yours',\n 'b007rn05': 'Weather',\n 'b006qptc': 'World at One',\n 'b006qpgr': 'The Archers',\n 'b04xxp0g': 'Drama',\n 'b006qjnv': 'Money Box',\n 'b019dl1b': 'Inside Health',\n 'm000s2kt': 'Sideways',\n 'b00dv9hq': 'The Media Show',\n 'b006qskw': 'PM',\n 'b006qjxt': \"Six O'Clock News\",\n 'b006qsq5': 'Front Row',\n 'b006qk11': 'Moral Maze',\n 'b006xp1x': 'Lent Talks',\n 'b006r4wn': 'Costing the Earth',\n 'b006qtl3': 'The World Tonight',\n 'm000czyb': 'The Skewer',\n 'b006qtqd': 'Today in Parliament'}\n```\n\nTo obtain a `ProgrammeCatalogue` with genres (be warned — for the last 30 days this takes 60 seconds):\n\n```py\nbeeb.api.get_genre_programme_dict(\"r4\", n_days=30)\n```\n⇣\n```py\n{'Arts': [('b006v8jn', 'A Good Read')],\n 'Arts, Culture \u0026 the Media': [('b01875r3', 'One to One'),\n                               ('b006qsq5', 'Front Row'),\n                               ('b00dv9hq', 'The Media Show'),\n                               ('b006qp6p', 'Open Book'),\n                               ('b006r5jt', 'The Film Programme'),\n                               ('b006slnx', 'Feedback'),\n                               ('m00055q2', 'Rewinder'),\n                               ('b006qjym', 'Loose Ends'),\n                               ('b06p0p0g', 'The Why Factor'),\n                               ('b006qpdd', 'Pick of the Week'),\n                               ('b006r9xr', 'Start the Week'),\n                               ('b006s5sf', 'Bookclub'),\n                               ('b09w07c4', 'Art of Now')],\n 'Biographical': [('b0721qqk', 'Riot Girls')],\n 'Chat': [('p04x5pd7', 'Fortunately... with Fi and Jane'),\n          ('m000s9s1', 'Between Ourselves with Marian Keyes'),\n          ('b00snr0w', 'The Infinite Monkey Cage')],\n 'Classic \u0026 Period': [('m000j0t9', 'Electric Decade')],\n 'Comedy': [('b00x8dq1', 'My Teenage Diary'),\n            ('b08mj1wj', 'Reluctant Persuaders'),\n            ('m0002rjm', \"Alexei Sayle's The Absence of Normal\")],\n 'Consumer': [('b006qps9', 'You and Yours')],\n 'Crime \u0026 Justice': [('b006tgy1', 'Law in Action'), ('m0000nfh', 'Intrigue')],\n 'Disability': [('b006qxww', 'In Touch')],\n 'Drama': [('b04xxp0g', 'Drama'),\n           ('b08lw2hh', 'Short Works'),\n           ('m000s855', \"Hardy's Women\")],\n 'Entertainment': [('b060cdyj', 'Bunk Bed')],\n 'Factual': [('b006s54y', 'Selection of BBC World Service Programmes'),\n             ('b007qlvb', \"Woman's Hour\"),\n             ('m0001kbd', 'Born in Bradford'),\n             ('b006th08', 'File on 4'),\n             ('m000s2kt', 'Sideways'),\n             ('b006qk11', 'Moral Maze'),\n             ('b006qjlq', 'From Our Own Correspondent'),\n             ('b006qnc7', 'Radio 4 Appeal'),\n             ('b07cblx9', 'The Briefing Room'),\n             ('b006qng8', 'A Point of View'),\n             ('b006qnj3', 'Broadcasting House'),\n             ('m0003r3t', 'My Name Is...'),\n             ('m00019hp', 'Archive on 4'),\n             ('b007qxpr', 'Round Britain Quiz'),\n             ('b03w7bwg', 'Out of the Ordinary'),\n             ('b09zgd6y', 'Chinese Characters')],\n 'Families \u0026 Relationships': [('b006qgj4', 'Saturday Live')],\n 'Food \u0026 Drink': [('b006qnx3', 'The Food Programme')],\n 'Gardens': [('b006qp2f', \"Gardeners' Question Time\")],\n 'Health \u0026 Wellbeing': [('b019dl1b', 'Inside Health')],\n 'Historical': [('m000sqkk', 'Gudrun')],\n 'History': [('b006qykl', 'In Our Time'),\n             ('b00nrtd2', 'A History of the World in 100 Objects')],\n 'Life Stories': [('b01mk3f8', 'Short Cuts'),\n                  ('b006qnmr', 'Desert Island Discs'),\n                  ('b006qpmv', 'Last Word'),\n                  ('b006qjz5', 'Profile'),\n                  ('b03cdpww', 'Meeting Myself Coming Back'),\n                  ('b01cqx3b', 'The Listening Project')],\n 'Money': [('b006qjnv', 'Money Box'), ('b006sz6t', 'The Bottom Line')],\n 'Music': [('b00704s1', 'Counterpoint')],\n 'Nature \u0026 Environment': [('b006qj8q', 'Farming Today'),\n                          ('b006xrr2', 'Ramblings'),\n                          ('b05w99gb', 'Natural Histories'),\n                          ('b006r4wn', 'Costing the Earth')],\n 'News': [('b00cs19l', 'Midnight News'),\n          ('b007rhyn', 'News Briefing'),\n          ('b006qj9z', 'Today'),\n          ('b04fc120', 'News Summary'),\n          ('b006qptc', 'World at One'),\n          ('b006qskw', 'PM'),\n          ('b006qjxt', \"Six O'Clock News\"),\n          ('b006qtl3', 'The World Tonight'),\n          ('b007rhyy', 'News and Papers'),\n          ('b00g3j4x', 'News'),\n          ('b006qnz4', 'The World This Weekend')],\n 'Panel Shows': [('b006s5dp', 'Just a Minute')],\n 'Politics': [('b006qtqd', 'Today in Parliament'),\n              ('b006qgvj', 'Any Questions?'),\n              ('b006qjfq', 'The Week in Westminster'),\n              ('b006qmmy', 'Any Answers?'),\n              ('m0001xq1', 'The Battles That Won Our Freedoms'),\n              ('b006r4vz', 'Analysis'),\n              ('b006s624', 'Westminster Hour')],\n 'Religion \u0026 Ethics': [('b006qmpj', 'Prayer for the Day'),\n                       ('b006xp1x', 'Lent Talks'),\n                       ('b006sgsh', 'Bells on Sunday'),\n                       ('b006qn7f', 'Something Understood'),\n                       ('b006qnbd', 'Sunday'),\n                       ('b006qnds', 'Sunday Worship')],\n 'Satire': [('b09rwvwt', 'Henry Normal: A Normal...'),\n            ('m000czyb', 'The Skewer'),\n            ('b006qgt7', 'The Now Show')],\n 'Science \u0026 Nature': [('b01s6xyk', 'Tweet of the Day'),\n                      ('b07dx75g', 'The Curious Cases of Rutherford \u0026 Fry'),\n                      ('b036f7w2', 'BBC Inside Science'),\n                      ('b06vy2jd', 'Science Stories')],\n 'Science \u0026 Technology': [('b01n7094', 'The Digital Human')],\n 'Sitcoms': [('b0b2nh1n', 'Ability')],\n 'Sketch': [('b00kvs8r', 'Newsjack')],\n 'Soaps': [('b006qpgr', 'The Archers'), ('b006qnkc', 'The Archers Omnibus')],\n 'Standup': [('b0b22qhr', 'Stand-Up Specials'),\n             ('b011tzjy', 'Meet David Sedaris')],\n 'Weather': [('b006qfvv', 'Shipping Forecast'), ('b007rn05', 'Weather')]}\n```\n\nThis is equivalent to constructing the class directly and accessing its `.keyed_by_genre` property.\n\n```py\nbeeb.nav.ProgrammeCatalogue(\"r4\", n_days=30, with_genre=True)\n```\n\n- This takes about 7 or 8 seconds (fast given the number of requests it's making!)\n- Note that these requests occasionally fail (async sessions are prone to rare connection errors),\n  and are silently retried up to 3 times (one seems to be enough in my experience).\n\nThe programme catalogues can be stored in a database and then restored from there:\n\n```py\n\u003e\u003e\u003e pc = beeb.nav.ProgrammeCatalogue(\"r4\", n_days=1, with_genre=True, async_pull=True)'\n\u003e\u003e\u003e pc.store_db()\n\u003e\u003e\u003e rc = beeb.nav.ProgrammeCatalogue.regenerate_from_db(\"r4\")\n\u003e\u003e\u003e pc == rc, pc.genred == rc.genred, pc.db.path == rc.db.path, type(rc) is type(pc)\n(True, True, True, True)\n```\n\nNote that regenerated catalogues have no associated day range\n\n```py\n\u003e\u003e\u003e rc.n_days\n0\n```\n\nThe SQLite3 database `programme_catalogue.db` records a simple four field table, `programmes`, in `beeb.data.store`:\n\n```sql\nSELECT * FROM programmes;\n```\n⇣\n```STDOUT\nm000tcdg|Midnight News|News|r4\nm000tcdj|Shipping Forecast|Weather|r4\nm000tcdl|Selection of BBC World Service Programmes|Factual|r4\n...\n```\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\nIf your programme(s) of interest are regular, long-running ones, they should be\nincluded in the database of programmes which ships with beeb.\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for ProgrammeCatalogue database support and multiple channel catalogues in ProgrammeGuide\u003c/summary\u003e\n\n\u003cp\u003e\n\nMultiple `ProgrammeCatalogue` objects can also be produced in a `ProgrammeGuide`.\nThis is a glorified dictionary, keyed by the station name and with the catalogues\nas its values.\n\nTo create a `ProgrammeGuide` for an entire categories of stations (available\ncategories: \"national\", \"regional\", \"local\"), and automatically store them in\na database, call `ProgrammeGuide.generate_by_category(c)`,\nwhere `c` is either one of the options, or a list of them.\n\nThe package ships with a catalogue database populated with the national channels\n(excluding variants: only FM 'outlets' for those with multiple which share a station title).\n\nPopulating the programme catalogues database for all national channels\ntakes approximately 60 to 90 seconds, and then can be reloaded instantly:\n\n```py\nbeeb.nav.ProgrammeGuide.generate_by_category(\"national\")'\n```\n\nUnlike the episodes in `ChannelSchedule` and `ChannelListings`, this is\n(potentially) available at runtime, instantly, making it preferable for\nmetadata lookup in some cases.\n\n```py\n\u003e\u003e\u003e guide = beeb.nav.ProgrammeGuide.generate_by_category(\"national\")\n\u003e\u003e\u003e guide.get_programme_by_title(\"Today\")\n('b006qj9z', ('Today', 'News'))\n\u003e\u003e\u003e guide.get_programme_by_title(\"Today\", pid_only=True)\n'b006qj9z'\n```\n\nThe following SQL query shows duplicates in the resulting database\n([via](https://stackoverflow.com/a/36384145/2668831)). Note that these are\npermitted as the primary key consists of both the programme ('brand') PID\nand the station short name (corresponding to the channel's entry in\n`beeb.nav.channel_ids`).\n\nThis is not an error: these shows are simply syndicated across channels.\n\n```sql\nSELECT *\nFROM programmes\nWHERE pid IN (SELECT pid\n              FROM programmes\n              GROUP BY pid\n              HAVING COUNT(pid) \u003e1);\n```\n⇣\n```\nb006wkp7|Annie Nightingale presents...|Dance \u0026 Electronica|r1\nb006wkp7|Annie Nightingale presents...|Dance \u0026 Electronica|r1x\nb006wkry|Newsbeat|News|an\nb006wkry|Newsbeat|News|r1\nb006wkry|Newsbeat|News|r1x\n...\n```\n\nSee\n[`beeb.nav.cat.catalogue`](https://github.com/lmmx/beeb/blob/master/src/beeb/nav/cat/catalogue.py)\nand [`beeb.share.db_utils`](https://github.com/lmmx/beeb/blob/master/src/beeb/share/db_utils.py)\nfor more details.\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n## Stream handling\n\n\u003cdetails\u003e\u003csummary\u003e❧ Click here for the basics of stream handling\u003c/summary\u003e\n\n\u003cp\u003e\n\nEpisodes are downloaded from BBC Sounds as M4S (MPEG-DASH streams). There is a header `.dash` file\nand then multiple `.dash` files, and if you have all of these you can build a MP4 audio file.\n\n\u003e As far as I know the access to these is geo-fenced, i.e. you must\n\u003e be in the UK to download but this may vary between programmes/stations.\n\nTo download an episode, initialise a `Stream` object, which will pull the component MPEG-DASH stream\nparts, merge them, and transcode from MP4 to WAV at 16k, as is desirable for most audio handling\n(but note this transcoding does increase the filesize). By default, the individual parts are deleted\nafter download.\n\nThe default download directory is a package-internal path beneath `beeb.data.store`, followed by\na subpath denoting: station » programme (by PID) » year » month » day. To change the directory,\nsubclass or otherwise set `_root_store_dir`.\n\n- Another option is to initialise with `defer_pull=False`, set the attribute, \n  then call the `Stream`'s `pull()` and `preprocess()` methods.\n\nTo download an episode by name and date, use the helper class method `Stream.from_name`:\n\n```py\nbeeb.stream.Stream.from_name(\"r4\", \"Today\", ymd=(2021,3,30))\n```\n\n\u003e The station ID (in this example, \"r4\") can be looked up in `channel_ids` or with the\n\u003e `ChannelPicker.by_name` helper class method.\n\nThe most directly useful functions (which I've needed when interacting with BBC Sounds API) are\nthose to obtain the M4S links: you only need the final one to construct the full set of URLs\nto obtain a complete MP4.\n\nThe following functions handle this in `beeb.api`:\n\n- `get_episode_dict`\n  - a trivial wrapper to access the `episodes_dict` attribute of `EpisodeListingsHtml`\n- `final_m4s_link_from_programme_pid`\n  - a wrapper to access the `last_m4s_link` attribute of the `MpdXml` class constructed with the `from_episode_pid` class method\n- `final_m4s_link_from_episode_pid`\n  - a wrapper to access the `last_m4s_link` attribute of the `MpdXml` class constructed with the `from_episode_pid` class method\n    after obtaining the episode PID from the episode dict\n- `get_programme_pid_by_name`\n  - a wrapper to access the `filtered` attribute of a `EpisodeMetadataPidJson` object\n    constructed with the `get_programme_pid` class method.\n  - technically it's \"by programme title and station name\" (the arguments are in this order)\n\nYou may very well prefer to construct the objects and handle the attributes involved yourself,\nthese are given as 'recipes' to make it clear how to use beeb's functionality.\n\n\u003c/p\u003e\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flmmx%2Fbeeb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flmmx%2Fbeeb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flmmx%2Fbeeb/lists"}