diff --git a/README.md b/README.md index 2f2fe5a..e70dafb 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,25 @@ Using the [example above](#example), the result will be: The configuration file is placed in home directory. -The name: `.podcast_downloader_config.json`. - -The format is: [JSON](https://en.wikipedia.org/wiki/JSON). +The name: `.podcast_downloader_config.json`. The file is format in [JSON](https://en.wikipedia.org/wiki/JSON). ### The settings hierarchy The script will replace default values by read from configuration file. Those will be cover by all values given by command line. +``` + command line parameters > configuration file > default values +``` + ### The main options -| Property | Type | Required | Default | Note | -|:---------------------|:---------:|:--------:|:------------------------:|:-----| -| `downloads_limit` | number | no | infinity | | -| `if_directory_empty` | string | no | download_last | See [In case of empty directory](#in-case-of-empty-directory) | -| `podcast_extensions` | key-value | no | `{".mp3": "audio/mpeg"}` | The file filter | +| Property | Type | Required | Default | Note | +|:---------------------|:----------:|:--------:|:------------------------:|:-----| +| `downloads_limit` | number | no | infinity | | +| `if_directory_empty` | string | no | download_last | See [In case of empty directory](#in-case-of-empty-directory) | +| `podcast_extensions` | key-value | no | `{".mp3": "audio/mpeg"}` | The file filter | +| `podcasts` | subsection | yes | `[]` | See [Podcasts sub category](#podcasts-sub-category) | ### Podcasts sub category @@ -134,8 +137,16 @@ Notes: the dot on the file extension is require. If a directory for podcast is empty, the script needs to recognize what to do. Due to lack of database, you can: -* download only the last episode -* download all new episode from last n days +* [download all episodes from feed](#download-all-from-feed) +* [download only the last episode](#only-last) +* [download all new episode from last n days](#download-all-from-n-days) +* [download all new episode since day after, the last episode should appear](#download-all-episode-since-last-excepted) + +### Download all from feed + +The script will download all episodes from the feed. + +Set by `download_all_from_feed`. ### Only last @@ -150,11 +161,34 @@ Set by `download_last`. The script will download all episodes which appear in last *n* days. I can be use when you are downloading on regular schedule. The *n* number is given within the setup value: `download_from_n_days`. For example: `download_from_3_days` means download all episodes from last 3 days. -### Download all from feed +### Download all episode since last excepted -The script will download all episodes from the feed. +The script will download all episodes which appear after the day of release of last episode. -Set by `download_all_from_feed`. +The *n* number is the day of the normal episode. +You can provide here week days as word (size of the letters is ignored) + +| Full week day | Shorten name | +|:--------------|:-------------| +| Monday | Mon | +| Tuesday | Tues | +| Wednesday | Weds | +| Thursday | Thurs | +| Friday | Fri | +| Saturday | Sat | +| Sunday | Sun | + +You can provide the number, it will means the day of the month. The script accepts only number from 1 to 28. + +Set by `download_from_`. + +Examples: + +| Example value | Meaning | +|------------------------|---------| +| `download_from_monday` | New episodes appear in Monday. The script will download all episodes since last Tuesday (including it) | +| `download_from_Fri` | New episodes appear in Friday. The script will download all episodes since last Saturday (including it) | +| `download_from_12` | New episodes appear each 12th of month. The script will download all episodes since 13 month before | ## The analyze of the RSS feed diff --git a/docker-compose.yml b/docker-compose.yml index 51a20e2..7859e93 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,7 @@ version: '3.9' services: - app: + tests: build: context: . volumes: - .:/app/ - \ No newline at end of file diff --git a/podcast_downloader/__main__.py b/podcast_downloader/__main__.py index d56f190..843dd43 100644 --- a/podcast_downloader/__main__.py +++ b/podcast_downloader/__main__.py @@ -8,7 +8,12 @@ from functools import partial from . import configuration -from podcast_downloader.configuration import configuration_verification, get_n_age_date +from podcast_downloader.configuration import ( + configuration_verification, + get_label_to_date, + get_n_age_date, + parse_day_label, +) from .utils import log, compose from .downloaded import get_extensions_checker, get_last_downloaded from .parameters import merge_parameters_collection, load_configuration_file, parse_argv @@ -62,11 +67,19 @@ def configuration_to_function( if configuration_value == "download_all_from_feed": return lambda source: source + local_time = time.localtime() + from_n_day_match = re.match(r"^download_from_(\d+)_days$", configuration_value) if from_n_day_match: - from_date = get_n_age_date(int(from_n_day_match[1]), time.localtime()) + from_date = get_n_age_date(int(from_n_day_match[1]), local_time) return only_entities_from_date(from_date) + from_nth_day_match = re.match(r"^download_from_(.*)", configuration_value) + if from_nth_day_match: + day_label = parse_day_label(from_nth_day_match[1]) + + return only_entities_from_date(get_label_to_date(day_label)(local_time)) + raise Exception(f"The value the '{configuration_value}' is not recognizable") diff --git a/podcast_downloader/configuration.py b/podcast_downloader/configuration.py index 0fb4094..84c5d26 100644 --- a/podcast_downloader/configuration.py +++ b/podcast_downloader/configuration.py @@ -1,4 +1,6 @@ -from typing import List, Tuple +from functools import partial +from typing import List, Tuple, Union +from datetime import datetime, timedelta import time SECONDS_IN_DAY = 24 * 60 * 60 @@ -14,6 +16,16 @@ CONFIG_PODCASTS_REQUIRE_DATE = "require_date" CONFIG_PODCASTS_DISABLE = "disable" +WEEK_DAYS = ( + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +) + def configuration_verification(config: dict) -> Tuple[bool, List[str]]: for podcast in config[CONFIG_PODCASTS]: @@ -37,3 +49,63 @@ def configuration_verification(config: dict) -> Tuple[bool, List[str]]: def get_n_age_date(day_number: int, from_date: time.struct_time) -> time.struct_time: return time.localtime(time.mktime(from_date) - day_number * SECONDS_IN_DAY) + + +def get_label_to_date(day_label: Union[str, int]) -> partial: + if day_label in WEEK_DAYS: + return partial(get_week_day, day_label) + + return partial(get_nth_day, int(day_label)) + + +def get_week_day(weekday_label: str, from_date: time.struct_time) -> time.struct_time: + from_datetime = datetime(*from_date[:6]) + weekday_from_date = from_datetime.weekday() + weekday_label_index = WEEK_DAYS.index(weekday_label) + result_datetime = from_datetime - timedelta( + 6 + if weekday_from_date == weekday_label_index + else weekday_from_date - weekday_label_index - 1 + ) + + return result_datetime.timetuple() + + +def get_nth_day(day: int, from_date: time.struct_time) -> time.struct_time: + from_datetime = datetime(*from_date[:6]) + + day_difference = from_date[2] - day + datetime_result = ( + from_datetime - timedelta(days=day_difference - 1) + if day_difference > 0 + else (from_datetime.replace(day=1) - timedelta(days=28)).replace(day=day + 1) + ) + + return datetime_result.timetuple() + + +def parse_day_label(raw_label: str) -> Union[str, int]: + if raw_label.isnumeric(): + return int(raw_label) + + if raw_label == "1st": + return 1 + + if raw_label == "2nd": + return 2 + + if raw_label == "3rd": + return 3 + + if raw_label[-2:] == "th": + return int(raw_label[:-2]) + + capitalize_raw_label = raw_label.capitalize() + if capitalize_raw_label in WEEK_DAYS: + return capitalize_raw_label + + short_weekdays = ("Mon", "Tues", "Weds", "Thurs", "Fri", "Sat", "Sun") + if capitalize_raw_label in short_weekdays: + return WEEK_DAYS[short_weekdays.index(capitalize_raw_label)] + + raise Exception(f"Cannot read weekday name '{raw_label}'") diff --git a/tests/get_all_entites_from_n_days_test.py b/tests/get_all_entites_from_n_days_test.py index 2b8c3d5..d538820 100644 --- a/tests/get_all_entites_from_n_days_test.py +++ b/tests/get_all_entites_from_n_days_test.py @@ -9,6 +9,7 @@ ) from commons import rss_entity_generator, build_timestamp + class TestAllEntitiesFromNDays(unittest.TestCase): def test_of_filter_function(self): # Assign diff --git a/tests/get_all_entries_from_th_day_test.py b/tests/get_all_entries_from_th_day_test.py new file mode 100644 index 0000000..eb5bf2b --- /dev/null +++ b/tests/get_all_entries_from_th_day_test.py @@ -0,0 +1,128 @@ +from time import strptime +import unittest + +from podcast_downloader.configuration import ( + WEEK_DAYS, + get_label_to_date, + get_nth_day, + get_week_day, + parse_day_label, +) + + +class TestParseDayLabel(unittest.TestCase): + def test_parse_day_label_correct_values(self): + test_parameters = { + "Monday": "Monday", + "mon": "Monday", + "wednesday": "Wednesday", + "fRIDay": "Friday", + "saturday": "Saturday", + "1st": 1, + "2nd": 2, + "3rd": 3, + "4th": 4, + "6": 6, + "25th": 25, + } + + for test_value, expected_value in test_parameters.items(): + result = parse_day_label(test_value) + self.assertEqual(result, expected_value, "Day of week incorrect recognized") + + def test_parse_day_label_incorrect_values(self): + self.assertRaises(Exception, parse_day_label, "abcde") + + def test_get_label_to_date_correct_values(self): + for week_day in WEEK_DAYS: + result = get_label_to_date(week_day) + self.assertEqual(result.func, get_week_day) + + for day_number in range(1, 32): + result = get_label_to_date(day_number) + self.assertEqual(result.func, get_nth_day) + + +class TestGetWeekDay(unittest.TestCase): + """Used in examples: + + Mo Tu We Th Fr Sa Su + 17 18 19 20 21 22 23 + 24 25 26 27 28 29 30 + """ + + def test_for_day_before(self): + # Assign + current_date = strptime("29.10.2022", "%d.%m.%Y") + day_after_last_monday = strptime("25.10.2022", "%d.%m.%Y") + + # Act + result = get_week_day(WEEK_DAYS[0], current_date) # Monday + + # Assert + self.assertEqual( + result, day_after_last_monday, "Expected first Tuesday before the date" + ) + + def test_for_day_in_the_day(self): + # Assign + current_date = strptime("26.10.2022", "%d.%m.%Y") + day_after_last_wednesday = strptime("20.10.2022", "%d.%m.%Y") + + # Act + result = get_week_day(WEEK_DAYS[2], current_date) # Wednesday + + # Assert + self.assertEqual( + result, day_after_last_wednesday, "Expected first Thursday before the date" + ) + + +class TestGetNthDay(unittest.TestCase): + def test_for_day_inside_the_month(self): + # Assign + nth_day = 2 + current_date = strptime("23.10.2022", "%d.%m.%Y") + expected_date = strptime(f"{nth_day + 1}.10.2022", "%d.%m.%Y") + + # Act + result = get_nth_day(nth_day, current_date) + + # Assert + self.assertEqual( + result, + expected_date, + f"Expected return the {nth_day + 1} of the same month", + ) + + def test_for_day_month_before(self): + # Assign + nth_day = 17 + current_date = strptime("4.10.2022", "%d.%m.%Y") + expected_date = strptime(f"{nth_day + 1}.09.2022", "%d.%m.%Y") + + # Act + result = get_nth_day(nth_day, current_date) + + # Assert + self.assertEqual( + result, + expected_date, + f"Expected return the {nth_day} of the previous month", + ) + + def test_for_day_month_before_january(self): + # Assign + nth_day = 17 + current_date = strptime("4.01.2022", "%d.%m.%Y") + expected_date = strptime(f"{nth_day + 1}.12.2021", "%d.%m.%Y") + + # Act + result = get_nth_day(nth_day, current_date) + + # Assert + self.assertEqual( + result, + expected_date, + f"Expected return the {nth_day} of the December", + )