![IPyTV logo](doc/logo_hd.png "IPyTV logo")
* **ipytv**: A python3 library to parse IPTV playlists in the M3U Plus format.
* **[iptv2json](ipytv/cli/README.md#iptv2json)**: A command line utility to convert an IPTV
playlist into json format.
* **[json2iptv](ipytv/cli/README.md#json2iptv)**: A command line utility to convert a json file
(like the one produced by `iptv2json`) into an IPTV (m3u_plus) playlist.
[![Downloads](https://pepy.tech/badge/m3u-ipytv)](https://pepy.tech/project/m3u-ipytv)
[![Downloads](https://pepy.tech/badge/m3u-ipytv/month)](https://pepy.tech/project/m3u-ipytv)
[![Downloads](https://pepy.tech/badge/m3u-ipytv/week)](https://pepy.tech/project/m3u-ipytv)
## M3U Plus and IPTV
The M3U Plus format is a _de facto_ standard for distributing IPTV playlists on
the Internet.
The terms _IPTV playlist_ and _M3U Plus playlist_ are generally used
interchangeably, but in this repository **M3U Plus** refers to the data format,
while **IPTV Playlist** refers to playlists in the M3U Plus format.
M3U Plus stems from the
[`extended M3U8`](https://en.wikipedia.org/wiki/M3U#Extended_M3U) format, of
which it supports only 2 tags (`#EXTM3U` and `#EXTINF`).
The syntax of the `#EXTM3U` and `#EXTINF` tags has been modified to include
extra attributes (e.g., logo, group, language). Unfortunately this has broken
the backward compatibility with the original M3U8 standard (as explained in
detail [here](#format-considerations)).
This library has been created from scratch to parse and handle the M3U Plus
format only. It does not fully support regular M3U8 playlists (only basic
channel attributes are parsed).
### Supported tags
Only `#EXTM3U`, `#EXTINF` and plain url rows are supported (i.e. they are parsed
and their value is made available as properties of an `IPTVChannel` object).
All tags that are found between an `#EXTINF` row and its related url row are
added as `extras` to a channel, but without performing any parsing (i.e. they're
treated like plain strings).
In the example below, the `#EXTVLCOPT` row is not parsed, but copied _as-is_:
```text
#EXTINF:-1 tvg-id="" tvg-name="hello" tvg-country="IT" tvg-url="" group-title="Greetings",Hello!
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0
https://my-website.com/vod/hello.ts
```
## Installation
This library requires Python 3 (and the related `pip` installer).
**PLEASE NOTE**: the library makes use of the `multiprocessing.Pool` class that
requires some care when working with the
[IDLE](https://docs.python.org/3/library/idle.html) environment.
To install the library system-wide, run:
```shell
pip install m3u-ipytv
```
To install it within a virtual environment, run:
```shell
python -m venv .venv
source .venv/bin/activate
pip install m3u-ipytv
```
## Usage
### Modules
The library comprises several modules, each with a specific area of competence:
- **channel**
- Everything related to the handling of channels in a playlist.
- **doctor**
- A collection of functions to fix common errors found in M3U files.
- **exceptions**
- All the exceptions thrown by the library.
- **m3u**
- Constants and functions related to M3U files.
- **playlist**
- Everything related to the loading and handling of M3U playlists.
- **utils**
- Commodity functions that work on playlist or channel objects.
### Loading an IPTV Playlist
#### From a file
Use the `playlist.loadf(file)` function:
```python
from ipytv import playlist
file = "~/Documents/my_playlist.m3u"
pl = playlist.loadf(file)
print(pl.length())
```
#### from a URL
Use the `playlist.loadu(url)` function:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/classic.m3u"
pl = playlist.loadu(url)
print(pl.length())
```
#### From a string
Use the `playlist.loads(string)` function:
```python
from ipytv import playlist
string = """#EXTM3U
#EXTINF:-1 tvg-id="Rai 1" tvg-name="Rai 1" group-title="RAI",Rai 1
http://myown.link:80/luke/210274/78482"""
pl = playlist.loads(string)
print(pl.length())
```
#### From a list
Use the `playlist.loadl(rows)` function:
```python
from ipytv import playlist
rows = [
'#EXTM3U',
'#EXTINF:-1 tvg-id="Rai 1" tvg-name="Rai 1" group-title="RAI",Rai 1',
'http://myown.link:80/luke/210274/78482'
]
pl = playlist.loadl(rows)
print(pl.length())
```
#### From a json string
Use the `playlist.loadj(json_str)` function:
```python
from ipytv import playlist
json_str = """{
"attributes": {
"x-tvg-url": "http://myown.link:80/luke/220311/22311"
},
"channels": [
{
"name": "Rai 1",
"duration": "-1",
"url": "http://myown.link:80/luke/210274/78482",
"attributes": {
"tvg-id": "Rai 1",
"tvg-name": "Rai 1",
"tvg-logo": "https://static.epg.best/it/RaiUno.it.png",
"group-title": "RAI"
},
"extras": [
"#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"
]
},
{
"name": "Rai 2",
"duration": "-1",
"url": "http://myown.link:80/luke/210274/78483",
"attributes": {
"tvg-id": "Rai 2",
"tvg-name": "Rai 2",
"tvg-logo": "https://static.epg.best/it/RaiDue.it.png",
"group-title": "RAI"
},
"extras": []
}
]
}"""
pl = playlist.loadj(json_str)
print(pl.length())
```
### M3UPlaylist class
Every load function above returns an object of the `M3UPlaylist` class.
This class models the concept of a playlist (which is, basically, a list of
channels) and offers methods to interact with the playlist itself and with its
channels.
There are two main properties in a playlist, and they are:
1. Attributes
2. Channels
What these properties are and how they can be accessed is described in the next
paragraphs.
### Accessing the attributes of a playlist
Key-value pairs that are specified in the `#EXTM3U` row are treated as
playlist-wide attributes (i.e. they apply to the playlist itself or to every
channel in the playlist).
For example the `x-tvg-url` part below:
```text
#EXTM3U x-tvg-url="http://myown.link:80/luke/220311/22311"
```
These attributes, in the form of a dictionary, can be accessed via the
`get_attributes()` method:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/kids.m3u"
pl = playlist.loadu(url)
attributes = pl.get_attributes()
for k, v in attributes.items():
print(f'"{k}": "{v}"')
```
In alternative, when the name of the property is known beforehand, its value can
be retrieved with:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/kids.m3u"
pl = playlist.loadu(url)
attributes = pl.get_attributes()
tvg_url = pl.get_attribute("x-tvg-url")
print(f"x-tvg-url: {tvg_url}")
```
The attributes can also be added, modified and removed by using the following
methods:
```python
from ipytv.playlist import M3UPlaylist
pl = M3UPlaylist()
attribute_name = 'tvg-shift'
# Add the 'tvg-shift' attribute and set it to 1
pl.add_attribute(attribute_name, "1")
# Update the 'tvg-shift' attribute to -2
pl.update_attribute(attribute_name, "-2")
# Completely remove the 'tvg-shift' attribute
value_before_deletion = pl.remove_attribute(attribute_name)
```
There is also a method that allows to add multiple attributes at once (instead
of single attributes) in the form of a dictionary:
```python
pl.add_attributes({})
```
### Accessing the channels of a playlist
The `M3UPlaylist` class is basically a list of channels with some commodity
functions. The channels in a playlist can be accessed by using one of the
following methods.
#### Individually (by index)
By using the `get_channel(index)` method:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/classic.m3u"
pl = playlist.loadu(url)
# Let's retrieve the first channel in the list
channel = pl.get_channel(0)
print(f'channel \"{channel.name}\": {channel.url}')
# The next line will throw IndexOutOfBoundsException
channel = pl.get_channel(-1)
```
#### Iteratively
By looping over the channels in an `M3UPlaylist` object:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/classic.m3u"
pl = playlist.loadu(url)
for channel in pl:
print(f'channel \"{channel.name}\": {channel.url}')
```
#### Low level
In all cases where the previous two access methods are not sufficient, the inner
channel list can be accessed via the `get_channels()` method:
```python
from ipytv import playlist
url = "https://iptv-org.github.io/iptv/categories/classic.m3u"
pl = playlist.loadu(url)
chan_list = pl.get_channels()
ten_channels = chan_list[:10]
```
The channels can also be added, modified and removed by using the following
methods:
```python
from ipytv.playlist import M3UPlaylist
from ipytv.channel import IPTVChannel
pl = M3UPlaylist()
channel = IPTVChannel()
# Add a channel to the end of the list (last index)
pl.append_channel(channel)
# Insert a channel in the specified position (all succeeding channels are
# shifted right by 1 position)
pl.insert_channel(0, channel)
new_channel = IPTVChannel()
# Replace the second channel of the playlist with a new channel
pl.update_channel(1, new_channel)
# Remove the channel at the specified position (all succeeding channels are
# shifted left by 1 position)
old_channel = pl.remove_channel(0)
```
There are also two methods that allow to add list of channels (instead of single
channels):
```python
pl.append_channels([])
pl.insert_channels([])
```
### Accessing the properties of a channel
The `get_channels()` method of an M3UPlaylist object returns a list of objects
of the `IPTVChannel` class.
An `IPTVChannel` object has 3 basic properties (`url`, `name` and
`duration`) and two optional fields: `attributes` (a dictionary) and `extras`
(a list of strings).
For example:
```python
from ipytv.channel import IPTVAttr, IPTVChannel
channel = IPTVChannel(
url="http://myown.link:80/luke/210274/78482",
name="Rai 1",
duration="-1",
attributes={
IPTVAttr.TVG_ID.value: "Rai 1",
IPTVAttr.TVG_NAME.value: "Rai 1",
IPTVAttr.TVG_LOGO.value: "https://static.epg.best/it/RaiUno.it.png",
IPTVAttr.GROUP_TITLE.value: "RAI"
},
extras=['#EXTVLCOPT:http-user-agent=Lavf53.32.100']
)
print(channel.name)
print(channel.attributes[IPTVAttr.GROUP_TITLE.value])
print(channel.extras[0])
```
The `IPTVAttr` enum class contains attribute names that are commonly found in
IPTV Playlists.
### The `doctor` module
Internet-sourced IPTV playlists, often contain a number of format errors. This
module aims to detect and fix some of these errors.
The module contains three classes, each with its own scope:
1. `M3UDoctor`
- It contains methods to fix errors in m3u files (i.e. errors that would
make it impossible to load an m3u file as a playlist).
2. `IPTVChannelDoctor`
- It contains methods to fix errors in a channel (i.e. errors in the
attributes of an #EXTINF row).
3. `M3UPlaylistDoctor`
- It applies the fixes in `IPTVChannelDoctor` to all channels in the
playlist.
All the classes above offer one public, static method named `sanitize()` that is
in charge of applying all different fixes. It can be used as follows:
```python
from ipytv.doctor import M3UDoctor, M3UPlaylistDoctor
from ipytv import playlist
with open('my-broken-playlist.m3u', encoding='utf-8') as in_file:
content = in_file.readlines()
fixed_content = M3UDoctor.sanitize(content)
pl = playlist.loadl(fixed_content)
fixed_pl = M3UPlaylistDoctor.sanitize(pl)
with open('my-fixed-playlist.m3u', 'w', encoding='utf-8') as out_file:
content = fixed_pl.to_m3u_plus_playlist()
out_file.write(content)
```
### The `utils` module
The `utils` module is a collection of commodity functions that perform various operations on
playlists or channels.
The module currently contains the functions listed below (but more might be added in the future).
-----
#### is_episode_from_series(channel.name)
A function that, given a channel name, checks whether the channel might be from a series or not.
Example:
```python
from ipytv.utils import is_episode_from_series
channel_name = "The Talking Dead S01 E07"
if is_episode_from_series(channel_name):
print("This channel looks like an episode from a series")
```
-----
#### extract_show_name(channel.name)
A function that, given a channel name, extracts only the show name, by removing the season and
episode numbers (if any) and every other string following these numbers (if any).
Example:
```python
from ipytv.utils import is_episode_from_series, extract_show_name
channel_name = "The Talking Dead S01 E07"
if is_episode_from_series(channel_name):
show_name = extract_show_name(channel_name)
print(f"This channel looks like an episode from the {show_name} series")
```
-----
#### extract_series(playlist, exclude_single=False)
A function that, given an M3UPlaylist object, tries to find all channels that look like episodes
of the same series and groups them together in a new playlist. The function returns a dictionary
with show names as keys and the related playlist as value. It also returns an extra playlist
that contains all the channels that are not episodes of a series. The `exclude_single` optional
parameter controls whether the function should return playlists with only one episode or not
(this is because, by definition, a series should be composed by at least two episodes, so the
function allows to remove all playlists with a single entry from the result).
Example:
```python
import os
from ipytv.playlist import loadu
from ipytv.utils import extract_series
pl = loadu("https://mametchikitty.github.io/Listas-IPTV/dibujos-animados.m3u")
series_map, not_series_pl = extract_series(pl, exclude_single=True)
out_dir = "./series"
os.makedirs(out_dir)
with open(f"{out_dir}/not_series.m3u", "w") as out_file:
out_file.write(not_series_pl.to_m3u_plus_playlist())
for show_title, pl in series_map.items():
# Slashes and colons are not allowed as filenames
show_filename = show_title.replace("/", "_").replace(":", "_")
with open(f"{out_dir}/{show_filename}.m3u", "w") as out_file:
out_file.write(pl.to_m3u_plus_playlist())
```
### Logging
IPyTV supports python's
standard [logging system](https://docs.python.org/3/library/logging.html).
To enable IPyTV's logging, add a logging configuration to your application:
```python
import logging
from ipytv import playlist
logging.basicConfig(level=logging.INFO)
pl = playlist.loadu("https://iptv-org.github.io/iptv/categories/classic.m3u")
```
### Object serialization
An M3UPlaylist object can be serialized into the following formats:
- M3U Plus (i.e. a string in the M3U Plus format):
- `pl.to_m3u_plus_playlist()`
- M3U8 (i.e. a string in the M3U8 format that can be parsed using the standard `m3u8` library):
- `pl.to_m3u8_playlist()`
- JSON (i.e. a JSON string that can be parsed using the standard `json` library):
- `pl.to_json_playlist()`
#### JSON format
The `pl.to_json_playlist()` method returns a JSON string that represents the playlist according to
the following format:
```json
{
"attributes": {
"x-tvg-url": "http://myown.link:80/luke/220311/22311"
},
"channels": [
{
"name": "Rai 1",
"duration": "-1",
"url": "http://myown.link:80/luke/210274/78482",
"attributes": {
"tvg-id": "Rai 1",
"tvg-name": "Rai 1",
"tvg-logo": "https://static.epg.best/it/RaiUno.it.png",
"group-title": "RAI"
},
"extras": [
"#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"
]
},
{
"name": "Rai 2",
"duration": "-1",
"url": "http://myown.link:80/luke/210274/78483",
"attributes": {
"tvg-id": "Rai 2",
"tvg-name": "Rai 2",
"tvg-logo": "https://static.epg.best/it/RaiDue.it.png",
"group-title": "RAI"
},
"extras": []
}
]
}
```
## Format considerations
The extensions to the `#EXTM3U` and `#EXTINF` tags introduced by the M3U Plus
format have broken the compatibility with the M3U8 format.
This is what a standard `#EXTINF` row should look like:
```text
#EXTINF:-1,Rai 1
```
The [format](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) is pretty
straightforward:
```text
#EXTINF:<duration>,[<title>]
```
Let's break it down:
1. The `#EXTINF:` tag.
1. The duration of the content (as an integer or float, signed or not).
1. A comma character.
1. A title.
This is what an `#EXTINF` row in the M3U Plus format looks like:
```text
#EXTINF:-1 tvg-id="Rai 1" tvg-name="Rai 1" tvg-logo="https://static.epg.best/it/RaiUno.it.png" group-title="RAI",Rai 1
```
If we break it down, we see that points 3. and 4. below have been added (and
they break the previous definition for the `#EXTINF` tag):
1. The `#EXTINF:` tag.
1. The duration of the content (as an integer or float, signed or not).
1. A space.
1. A variable-length, space-separated list of attributes.
1. A comma character.
1. A title.
The attributes in point 4 are in the `attribute="value"` format, where `value`
may also contain non-escaped commas and, sometimes, even unescaped double
quotes. This really complicates the parsing logic.
It's worth noting that the M3U8 RFC document specifies how
[attribute lists](https://tools.ietf.org/html/rfc8216#section-4.2) should be
formatted, but the M3U Plus implementation doesn't comply with the standard.
In conclusion, the M3U Plus format with its quirks and idiosyncrasies is hard to
read for humans and hard to parse for computers. It's an ugly format, but it's
too widespread to be ignored and for Python to lack a parsing library.
On a funny note, this is how the VLC programmers named the
[parsing function](https://github.com/videolan/vlc/blob/474c90392ede9916f068fcb3f860ba220d4c5b11/modules/demux/playlist/m3u.c#L398)
for the IPTV playlists in the M3U Plus format:
```c
static void parseEXTINFIptvDiots(...)
```
Just saying... :sweat_smile:
## License
This project is licensed under the terms of the MIT license.
See [LICENSE.txt](./LICENSE.txt) for details.
Raw data
{
"_id": null,
"home_page": "https://github.com/Beer4Ever83/ipytv",
"name": "m3u-ipytv",
"maintainer": null,
"docs_url": null,
"requires_python": "<4,>=3.6",
"maintainer_email": null,
"keywords": "m3u, m3u_plus, iptv, playlist",
"author": "Francesco Rainone",
"author_email": "beer4evah@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/1b/48/b1c67ac3c0162d56a228f72c5c2179cb05c046269759943893f2dda78985/m3u-ipytv-0.2.11.tar.gz",
"platform": null,
"description": "![IPyTV logo](doc/logo_hd.png \"IPyTV logo\")\n\n* **ipytv**: A python3 library to parse IPTV playlists in the M3U Plus format.\n* **[iptv2json](ipytv/cli/README.md#iptv2json)**: A command line utility to convert an IPTV \n playlist into json format.\n* **[json2iptv](ipytv/cli/README.md#json2iptv)**: A command line utility to convert a json file \n (like the one produced by `iptv2json`) into an IPTV (m3u_plus) playlist.\n\n[![Downloads](https://pepy.tech/badge/m3u-ipytv)](https://pepy.tech/project/m3u-ipytv)\n[![Downloads](https://pepy.tech/badge/m3u-ipytv/month)](https://pepy.tech/project/m3u-ipytv)\n[![Downloads](https://pepy.tech/badge/m3u-ipytv/week)](https://pepy.tech/project/m3u-ipytv)\n\n## M3U Plus and IPTV\n\nThe M3U Plus format is a _de facto_ standard for distributing IPTV playlists on\nthe Internet.\n\nThe terms _IPTV playlist_ and _M3U Plus playlist_ are generally used\ninterchangeably, but in this repository **M3U Plus** refers to the data format,\nwhile **IPTV Playlist** refers to playlists in the M3U Plus format.\n\nM3U Plus stems from the\n[`extended M3U8`](https://en.wikipedia.org/wiki/M3U#Extended_M3U) format, of \nwhich it supports only 2 tags (`#EXTM3U` and `#EXTINF`).\n\nThe syntax of the `#EXTM3U` and `#EXTINF` tags has been modified to include\nextra attributes (e.g., logo, group, language). Unfortunately this has broken\nthe backward compatibility with the original M3U8 standard (as explained in\ndetail [here](#format-considerations)).\n\nThis library has been created from scratch to parse and handle the M3U Plus\nformat only. It does not fully support regular M3U8 playlists (only basic\nchannel attributes are parsed).\n\n### Supported tags\n\nOnly `#EXTM3U`, `#EXTINF` and plain url rows are supported (i.e. they are parsed\nand their value is made available as properties of an `IPTVChannel` object).\n\nAll tags that are found between an `#EXTINF` row and its related url row are\nadded as `extras` to a channel, but without performing any parsing (i.e. they're\ntreated like plain strings).\n\nIn the example below, the `#EXTVLCOPT` row is not parsed, but copied _as-is_:\n\n```text\n#EXTINF:-1 tvg-id=\"\" tvg-name=\"hello\" tvg-country=\"IT\" tvg-url=\"\" group-title=\"Greetings\",Hello!\n#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0 \nhttps://my-website.com/vod/hello.ts\n```\n\n## Installation\n\nThis library requires Python 3 (and the related `pip` installer).\n\n**PLEASE NOTE**: the library makes use of the `multiprocessing.Pool` class that\nrequires some care when working with the\n[IDLE](https://docs.python.org/3/library/idle.html) environment.\n\nTo install the library system-wide, run:\n\n```shell\npip install m3u-ipytv\n```\n\nTo install it within a virtual environment, run:\n\n```shell\npython -m venv .venv\nsource .venv/bin/activate\npip install m3u-ipytv\n```\n\n## Usage\n\n### Modules\n\nThe library comprises several modules, each with a specific area of competence:\n\n- **channel**\n - Everything related to the handling of channels in a playlist.\n- **doctor**\n - A collection of functions to fix common errors found in M3U files.\n- **exceptions**\n - All the exceptions thrown by the library.\n- **m3u**\n - Constants and functions related to M3U files.\n- **playlist**\n - Everything related to the loading and handling of M3U playlists.\n- **utils**\n - Commodity functions that work on playlist or channel objects.\n\n### Loading an IPTV Playlist\n\n#### From a file\n\nUse the `playlist.loadf(file)` function:\n\n```python\nfrom ipytv import playlist\n\nfile = \"~/Documents/my_playlist.m3u\"\npl = playlist.loadf(file)\nprint(pl.length())\n```\n\n#### from a URL\n\nUse the `playlist.loadu(url)` function:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/classic.m3u\"\npl = playlist.loadu(url)\nprint(pl.length())\n```\n\n#### From a string\n\nUse the `playlist.loads(string)` function:\n\n```python\nfrom ipytv import playlist\n\nstring = \"\"\"#EXTM3U\n#EXTINF:-1 tvg-id=\"Rai 1\" tvg-name=\"Rai 1\" group-title=\"RAI\",Rai 1\nhttp://myown.link:80/luke/210274/78482\"\"\"\npl = playlist.loads(string)\nprint(pl.length())\n```\n\n#### From a list\n\nUse the `playlist.loadl(rows)` function:\n\n```python\nfrom ipytv import playlist\n\nrows = [\n '#EXTM3U',\n '#EXTINF:-1 tvg-id=\"Rai 1\" tvg-name=\"Rai 1\" group-title=\"RAI\",Rai 1',\n 'http://myown.link:80/luke/210274/78482'\n]\npl = playlist.loadl(rows)\nprint(pl.length())\n```\n\n#### From a json string\n\nUse the `playlist.loadj(json_str)` function:\n\n```python\nfrom ipytv import playlist\n\njson_str = \"\"\"{\n \"attributes\": {\n \"x-tvg-url\": \"http://myown.link:80/luke/220311/22311\"\n },\n \"channels\": [\n {\n \"name\": \"Rai 1\",\n \"duration\": \"-1\",\n \"url\": \"http://myown.link:80/luke/210274/78482\",\n \"attributes\": {\n \"tvg-id\": \"Rai 1\",\n \"tvg-name\": \"Rai 1\",\n \"tvg-logo\": \"https://static.epg.best/it/RaiUno.it.png\",\n \"group-title\": \"RAI\"\n },\n \"extras\": [\n \"#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0\"\n ]\n },\n {\n \"name\": \"Rai 2\",\n \"duration\": \"-1\",\n \"url\": \"http://myown.link:80/luke/210274/78483\",\n \"attributes\": {\n \"tvg-id\": \"Rai 2\",\n \"tvg-name\": \"Rai 2\",\n \"tvg-logo\": \"https://static.epg.best/it/RaiDue.it.png\",\n \"group-title\": \"RAI\"\n },\n \"extras\": []\n }\n ]\n}\"\"\"\npl = playlist.loadj(json_str)\nprint(pl.length())\n```\n\n### M3UPlaylist class\n\nEvery load function above returns an object of the `M3UPlaylist` class.\n\nThis class models the concept of a playlist (which is, basically, a list of\nchannels) and offers methods to interact with the playlist itself and with its\nchannels.\n\nThere are two main properties in a playlist, and they are:\n\n1. Attributes\n2. Channels\n\nWhat these properties are and how they can be accessed is described in the next\nparagraphs.\n\n### Accessing the attributes of a playlist\n\nKey-value pairs that are specified in the `#EXTM3U` row are treated as\nplaylist-wide attributes (i.e. they apply to the playlist itself or to every\nchannel in the playlist).\n\nFor example the `x-tvg-url` part below:\n\n```text\n#EXTM3U x-tvg-url=\"http://myown.link:80/luke/220311/22311\"\n```\n\nThese attributes, in the form of a dictionary, can be accessed via the\n`get_attributes()` method:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/kids.m3u\"\npl = playlist.loadu(url)\nattributes = pl.get_attributes()\nfor k, v in attributes.items():\n print(f'\"{k}\": \"{v}\"')\n```\n\nIn alternative, when the name of the property is known beforehand, its value can\nbe retrieved with:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/kids.m3u\"\npl = playlist.loadu(url)\nattributes = pl.get_attributes()\ntvg_url = pl.get_attribute(\"x-tvg-url\")\nprint(f\"x-tvg-url: {tvg_url}\")\n```\n\nThe attributes can also be added, modified and removed by using the following\nmethods:\n\n```python\nfrom ipytv.playlist import M3UPlaylist\n\npl = M3UPlaylist()\nattribute_name = 'tvg-shift'\n# Add the 'tvg-shift' attribute and set it to 1\npl.add_attribute(attribute_name, \"1\")\n# Update the 'tvg-shift' attribute to -2\npl.update_attribute(attribute_name, \"-2\")\n# Completely remove the 'tvg-shift' attribute\nvalue_before_deletion = pl.remove_attribute(attribute_name)\n```\n\nThere is also a method that allows to add multiple attributes at once (instead\nof single attributes) in the form of a dictionary:\n\n```python\npl.add_attributes({})\n```\n\n### Accessing the channels of a playlist\n\nThe `M3UPlaylist` class is basically a list of channels with some commodity\nfunctions. The channels in a playlist can be accessed by using one of the\nfollowing methods.\n\n#### Individually (by index)\n\nBy using the `get_channel(index)` method:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/classic.m3u\"\npl = playlist.loadu(url)\n# Let's retrieve the first channel in the list\nchannel = pl.get_channel(0)\nprint(f'channel \\\"{channel.name}\\\": {channel.url}')\n# The next line will throw IndexOutOfBoundsException\nchannel = pl.get_channel(-1)\n```\n\n#### Iteratively\n\nBy looping over the channels in an `M3UPlaylist` object:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/classic.m3u\"\npl = playlist.loadu(url)\nfor channel in pl:\n print(f'channel \\\"{channel.name}\\\": {channel.url}')\n```\n\n#### Low level\n\nIn all cases where the previous two access methods are not sufficient, the inner\nchannel list can be accessed via the `get_channels()` method:\n\n```python\nfrom ipytv import playlist\n\nurl = \"https://iptv-org.github.io/iptv/categories/classic.m3u\"\npl = playlist.loadu(url)\nchan_list = pl.get_channels()\nten_channels = chan_list[:10] \n```\n\nThe channels can also be added, modified and removed by using the following\nmethods:\n\n```python\nfrom ipytv.playlist import M3UPlaylist\nfrom ipytv.channel import IPTVChannel\n\npl = M3UPlaylist()\nchannel = IPTVChannel()\n# Add a channel to the end of the list (last index)\npl.append_channel(channel)\n# Insert a channel in the specified position (all succeeding channels are\n# shifted right by 1 position)\npl.insert_channel(0, channel)\nnew_channel = IPTVChannel()\n# Replace the second channel of the playlist with a new channel\npl.update_channel(1, new_channel)\n# Remove the channel at the specified position (all succeeding channels are\n# shifted left by 1 position)\nold_channel = pl.remove_channel(0)\n```\n\nThere are also two methods that allow to add list of channels (instead of single\nchannels):\n\n```python\npl.append_channels([])\npl.insert_channels([])\n```\n\n### Accessing the properties of a channel\n\nThe `get_channels()` method of an M3UPlaylist object returns a list of objects\nof the `IPTVChannel` class.\n\nAn `IPTVChannel` object has 3 basic properties (`url`, `name` and\n`duration`) and two optional fields: `attributes` (a dictionary) and `extras`\n(a list of strings).\n\nFor example:\n\n```python\nfrom ipytv.channel import IPTVAttr, IPTVChannel\n\nchannel = IPTVChannel(\n url=\"http://myown.link:80/luke/210274/78482\",\n name=\"Rai 1\",\n duration=\"-1\",\n attributes={\n IPTVAttr.TVG_ID.value: \"Rai 1\",\n IPTVAttr.TVG_NAME.value: \"Rai 1\",\n IPTVAttr.TVG_LOGO.value: \"https://static.epg.best/it/RaiUno.it.png\",\n IPTVAttr.GROUP_TITLE.value: \"RAI\"\n },\n extras=['#EXTVLCOPT:http-user-agent=Lavf53.32.100']\n)\nprint(channel.name)\nprint(channel.attributes[IPTVAttr.GROUP_TITLE.value])\nprint(channel.extras[0])\n```\n\nThe `IPTVAttr` enum class contains attribute names that are commonly found in\nIPTV Playlists.\n\n### The `doctor` module\n\nInternet-sourced IPTV playlists, often contain a number of format errors. This\nmodule aims to detect and fix some of these errors.\n\nThe module contains three classes, each with its own scope:\n\n1. `M3UDoctor`\n - It contains methods to fix errors in m3u files (i.e. errors that would\n make it impossible to load an m3u file as a playlist).\n2. `IPTVChannelDoctor`\n - It contains methods to fix errors in a channel (i.e. errors in the\n attributes of an #EXTINF row).\n3. `M3UPlaylistDoctor`\n - It applies the fixes in `IPTVChannelDoctor` to all channels in the\n playlist.\n\nAll the classes above offer one public, static method named `sanitize()` that is\nin charge of applying all different fixes. It can be used as follows:\n\n```python\nfrom ipytv.doctor import M3UDoctor, M3UPlaylistDoctor\nfrom ipytv import playlist\n\nwith open('my-broken-playlist.m3u', encoding='utf-8') as in_file:\n content = in_file.readlines()\n fixed_content = M3UDoctor.sanitize(content)\n pl = playlist.loadl(fixed_content)\n fixed_pl = M3UPlaylistDoctor.sanitize(pl)\n with open('my-fixed-playlist.m3u', 'w', encoding='utf-8') as out_file:\n content = fixed_pl.to_m3u_plus_playlist()\n out_file.write(content)\n```\n\n### The `utils` module\nThe `utils` module is a collection of commodity functions that perform various operations on \nplaylists or channels.\nThe module currently contains the functions listed below (but more might be added in the future).\n\n-----\n#### is_episode_from_series(channel.name)\nA function that, given a channel name, checks whether the channel might be from a series or not.\n\nExample:\n```python\nfrom ipytv.utils import is_episode_from_series\nchannel_name = \"The Talking Dead S01 E07\"\nif is_episode_from_series(channel_name):\n print(\"This channel looks like an episode from a series\")\n```\n\n-----\n#### extract_show_name(channel.name)\nA function that, given a channel name, extracts only the show name, by removing the season and \nepisode numbers (if any) and every other string following these numbers (if any).\n\nExample:\n```python\nfrom ipytv.utils import is_episode_from_series, extract_show_name\nchannel_name = \"The Talking Dead S01 E07\"\nif is_episode_from_series(channel_name):\n show_name = extract_show_name(channel_name)\n print(f\"This channel looks like an episode from the {show_name} series\")\n```\n\n-----\n#### extract_series(playlist, exclude_single=False)\nA function that, given an M3UPlaylist object, tries to find all channels that look like episodes \nof the same series and groups them together in a new playlist. The function returns a dictionary \nwith show names as keys and the related playlist as value. It also returns an extra playlist \nthat contains all the channels that are not episodes of a series. The `exclude_single` optional \nparameter controls whether the function should return playlists with only one episode or not \n(this is because, by definition, a series should be composed by at least two episodes, so the \nfunction allows to remove all playlists with a single entry from the result).\n\nExample:\n```python\nimport os\nfrom ipytv.playlist import loadu\nfrom ipytv.utils import extract_series\npl = loadu(\"https://mametchikitty.github.io/Listas-IPTV/dibujos-animados.m3u\")\nseries_map, not_series_pl = extract_series(pl, exclude_single=True)\nout_dir = \"./series\"\nos.makedirs(out_dir)\nwith open(f\"{out_dir}/not_series.m3u\", \"w\") as out_file:\n out_file.write(not_series_pl.to_m3u_plus_playlist())\n\nfor show_title, pl in series_map.items():\n # Slashes and colons are not allowed as filenames\n show_filename = show_title.replace(\"/\", \"_\").replace(\":\", \"_\")\n with open(f\"{out_dir}/{show_filename}.m3u\", \"w\") as out_file:\n out_file.write(pl.to_m3u_plus_playlist())\n```\n\n### Logging\n\nIPyTV supports python's\nstandard [logging system](https://docs.python.org/3/library/logging.html).\n\nTo enable IPyTV's logging, add a logging configuration to your application:\n\n```python\nimport logging\nfrom ipytv import playlist\n\nlogging.basicConfig(level=logging.INFO)\npl = playlist.loadu(\"https://iptv-org.github.io/iptv/categories/classic.m3u\")\n```\n\n### Object serialization\nAn M3UPlaylist object can be serialized into the following formats:\n- M3U Plus (i.e. a string in the M3U Plus format):\n - `pl.to_m3u_plus_playlist()`\n- M3U8 (i.e. a string in the M3U8 format that can be parsed using the standard `m3u8` library):\n - `pl.to_m3u8_playlist()`\n- JSON (i.e. a JSON string that can be parsed using the standard `json` library):\n - `pl.to_json_playlist()`\n\n#### JSON format\nThe `pl.to_json_playlist()` method returns a JSON string that represents the playlist according to \nthe following format:\n\n```json\n{\n \"attributes\": {\n \"x-tvg-url\": \"http://myown.link:80/luke/220311/22311\"\n },\n \"channels\": [\n {\n \"name\": \"Rai 1\",\n \"duration\": \"-1\",\n \"url\": \"http://myown.link:80/luke/210274/78482\",\n \"attributes\": {\n \"tvg-id\": \"Rai 1\",\n \"tvg-name\": \"Rai 1\",\n \"tvg-logo\": \"https://static.epg.best/it/RaiUno.it.png\",\n \"group-title\": \"RAI\"\n },\n \"extras\": [\n \"#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0\"\n ]\n },\n {\n \"name\": \"Rai 2\",\n \"duration\": \"-1\",\n \"url\": \"http://myown.link:80/luke/210274/78483\",\n \"attributes\": {\n \"tvg-id\": \"Rai 2\",\n \"tvg-name\": \"Rai 2\",\n \"tvg-logo\": \"https://static.epg.best/it/RaiDue.it.png\",\n \"group-title\": \"RAI\"\n },\n \"extras\": []\n }\n ]\n}\n```\n\n## Format considerations\n\nThe extensions to the `#EXTM3U` and `#EXTINF` tags introduced by the M3U Plus\nformat have broken the compatibility with the M3U8 format.\n\nThis is what a standard `#EXTINF` row should look like:\n\n```text\n#EXTINF:-1,Rai 1\n```\n\nThe [format](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) is pretty\nstraightforward:\n\n```text\n#EXTINF:<duration>,[<title>]\n```\n\nLet's break it down:\n\n1. The `#EXTINF:` tag.\n1. The duration of the content (as an integer or float, signed or not).\n1. A comma character.\n1. A title.\n\nThis is what an `#EXTINF` row in the M3U Plus format looks like:\n\n```text\n#EXTINF:-1 tvg-id=\"Rai 1\" tvg-name=\"Rai 1\" tvg-logo=\"https://static.epg.best/it/RaiUno.it.png\" group-title=\"RAI\",Rai 1\n```\n\nIf we break it down, we see that points 3. and 4. below have been added (and \nthey break the previous definition for the `#EXTINF` tag):\n\n1. The `#EXTINF:` tag.\n1. The duration of the content (as an integer or float, signed or not).\n1. A space.\n1. A variable-length, space-separated list of attributes.\n1. A comma character.\n1. A title.\n\nThe attributes in point 4 are in the `attribute=\"value\"` format, where `value`\nmay also contain non-escaped commas and, sometimes, even unescaped double \nquotes. This really complicates the parsing logic.\n\nIt's worth noting that the M3U8 RFC document specifies how\n[attribute lists](https://tools.ietf.org/html/rfc8216#section-4.2) should be\nformatted, but the M3U Plus implementation doesn't comply with the standard.\n\nIn conclusion, the M3U Plus format with its quirks and idiosyncrasies is hard to\nread for humans and hard to parse for computers. It's an ugly format, but it's\ntoo widespread to be ignored and for Python to lack a parsing library.\n\nOn a funny note, this is how the VLC programmers named the\n[parsing function](https://github.com/videolan/vlc/blob/474c90392ede9916f068fcb3f860ba220d4c5b11/modules/demux/playlist/m3u.c#L398)\nfor the IPTV playlists in the M3U Plus format:\n\n```c\nstatic void parseEXTINFIptvDiots(...)\n```\n\nJust saying... :sweat_smile:\n\n## License\n\nThis project is licensed under the terms of the MIT license.\n\nSee [LICENSE.txt](./LICENSE.txt) for details.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A library for handling M3U playlists for IPTV (AKA m3u_plus)",
"version": "0.2.11",
"project_urls": {
"Bug Reports": "https://github.com/Beer4Ever83/ipytv/issues",
"Funding": "https://www.buymeacoffee.com/beer4ever83",
"Homepage": "https://github.com/Beer4Ever83/ipytv",
"Source": "https://github.com/Beer4Ever83/ipytv"
},
"split_keywords": [
"m3u",
" m3u_plus",
" iptv",
" playlist"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "1b48b1c67ac3c0162d56a228f72c5c2179cb05c046269759943893f2dda78985",
"md5": "4f91f42b477e21a17c6936ad7edb8551",
"sha256": "20b69a98ffe459bef97d28680e759d86dbd66f9cae0b036af01ab180ffce4056"
},
"downloads": -1,
"filename": "m3u-ipytv-0.2.11.tar.gz",
"has_sig": false,
"md5_digest": "4f91f42b477e21a17c6936ad7edb8551",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4,>=3.6",
"size": 29388,
"upload_time": "2024-08-09T13:02:43",
"upload_time_iso_8601": "2024-08-09T13:02:43.296869Z",
"url": "https://files.pythonhosted.org/packages/1b/48/b1c67ac3c0162d56a228f72c5c2179cb05c046269759943893f2dda78985/m3u-ipytv-0.2.11.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-09 13:02:43",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Beer4Ever83",
"github_project": "ipytv",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"circle": true,
"requirements": [],
"lcname": "m3u-ipytv"
}