Skip to content
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

Batch Download Posts #64

Open
wants to merge 4 commits into
base: main
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ This is a rewrite/refactoring of [Avnsx](https://github.com/Avnsx)'s original [F

⚠️ Due to a [hashing bug](../../issues/13) duplicate videos might be downloaded if a creator re-posts a lot. Downloaded videos will have to be renamed in a future version when video hashing is perfected.

### v0.9.10 2024-06-28

Posts batch downloading ([#63](https://github.com/prof79/fansly-downloader-ng/issues/63)) kudos @1gintonic

### v0.9.9 2024-06-28

Accept URLs/be less picky when specifying single posts kudos ([#64](https://github.com/prof79/fansly-downloader-ng/issues/64)) @1gintonic
Expand Down
6 changes: 5 additions & 1 deletion ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

## 🗒️ Release Notes

### v0.9.10 2024-06-28

Posts batch downloading ([#63](https://github.com/prof79/fansly-downloader-ng/issues/63)) kudos @1gintonic

### v0.9.9 2024-06-28

Accept URLs/be less picky when specifying single posts kudos ([#64](https://github.com/prof79/fansly-downloader-ng/issues/64)) @1gintonic
Accept URLs/be less picky when specifying single posts ([#64](https://github.com/prof79/fansly-downloader-ng/issues/64)) kudos @1gintonic

### v0.9.8 2024-06-28

Expand Down
35 changes: 18 additions & 17 deletions config/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from errors import ConfigError
from textio import print_debug, print_warning
from utils.common import is_valid_post_id, save_config_or_raise, get_post_id_from_request
from utils.common import is_valid_post_id, save_config_or_raise, get_post_ids_from_list_of_requests


def parse_args() -> argparse.Namespace:
Expand Down Expand Up @@ -121,13 +121,13 @@ def parse_args() -> argparse.Namespace:
help='Use "Collection" download mode. This will ony download a collection.',
)
download_modes.add_argument(
'--single',
'--posts',
required=False,
default=None,
metavar='REQUESTED_POST',
dest='download_mode_single',
help='Use "Single" download mode. This will download a single post '
"by link or ID from an arbitrary creator. "
nargs='*',
dest='download_mode_posts',
help='Use "Posts" download mode. This will download all desired posts '
"by link or ID from arbitrary creators. Append all IDs separated by a whitespace."
"A post ID must be at least 10 characters and consist of digits only."
"Example - https://fansly.com/post/1283998432982 -> ID is: 1283998432982",
)
Expand Down Expand Up @@ -341,7 +341,7 @@ def map_args_to_config(args: argparse.Namespace, config: FanslyConfig) -> bool:

config_overridden = False
download_mode_set = False

config.debug = args.debug

if config.debug:
Expand Down Expand Up @@ -385,17 +385,18 @@ def map_args_to_config(args: argparse.Namespace, config: FanslyConfig) -> bool:
config_overridden = True
download_mode_set = True

if args.download_mode_single is not None:
post_id = get_post_id_from_request(args.download_mode_single)
config.download_mode = DownloadMode.SINGLE

if not is_valid_post_id(post_id):
raise ConfigError(
f"Argument error - '{post_id}' is not a valid post ID. "
"For an ID at least 10 characters/only digits are required."
)
if args.download_mode_posts is not None:
post_ids = get_post_ids_from_list_of_requests(args.download_mode_posts)
config.download_mode = DownloadMode.POSTS

for post_id in post_ids:
if not is_valid_post_id(post_id):
raise ConfigError(
f"Argument error - '{post_id}' is not a valid post ID. "
"For an ID at least 10 characters/only digits are required."
)

config.post_id = post_id
config.post_ids = post_ids
config_overridden = True
download_mode_set = True

Expand Down
2 changes: 1 addition & 1 deletion config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def load_config(config: FanslyConfig) -> None:
config._parser.get(options_section, 'download_directory', fallback='Local_directory')
)

# Normal (Timeline & Messages), Timeline, Messages, Single (Single by post id) or Collections -> str
# Normal (Timeline & Messages), Timeline, Messages, Posts (Posts by post ids) or Collections -> str
download_mode = config._parser.get(options_section, 'download_mode', fallback='Normal')
config.download_mode = DownloadMode(download_mode.upper())

Expand Down
6 changes: 3 additions & 3 deletions config/fanslyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from configparser import ConfigParser
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from typing import Optional, List

from config.modes import DownloadMode
from config.metadatahandling import MetadataHandling
Expand Down Expand Up @@ -35,7 +35,7 @@ class FanslyConfig(object):
token_from_browser_name: Optional[str] = None
debug: bool = False
# If specified on the command-line
post_id: Optional[str] = None
post_ids: Optional[List[str]] = None
# Set on start after self-update
updated_to: Optional[str] = None

Expand All @@ -57,7 +57,7 @@ class FanslyConfig(object):
#session_id: str = 'null'

# Options
# "Normal" | "Timeline" | "Messages" | "Single" | "Collection"
# "Normal" | "Timeline" | "Messages" | "Posts" | "Collection"
download_mode: DownloadMode = DownloadMode.NORMAL
download_directory: (None | Path) = None
download_media_previews: bool = True
Expand Down
2 changes: 1 addition & 1 deletion config/modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ class DownloadMode(StrEnum):
COLLECTION = auto()
MESSAGES = auto()
NORMAL = auto()
SINGLE = auto()
POSTS = auto()
TIMELINE = auto()
2 changes: 1 addition & 1 deletion download/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_creator_account_info(config: FanslyConfig, state: DownloadState) -> None
raise RuntimeError(message)

# Collections are independent of creators and
# single posts may diverge from configured creators
# posts may diverge from configured creators
if any([config.download_mode == DownloadMode.MESSAGES,
config.download_mode == DownloadMode.NORMAL,
config.download_mode == DownloadMode.TIMELINE]):
Expand Down
2 changes: 1 addition & 1 deletion download/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def process_download_accessible_media(
currently being downloaded.
:param list[dict] media_infos: A list of media informations from posts,
timelines, messages, collections and so on.
:param str|None post_id: The post ID required for "Single" download mode.
:param str|None post_id: The post ID required for "Posts" download mode.

:return: "False" as a break indicator for "Timeline" downloads,
"True" otherwise.
Expand Down
4 changes: 2 additions & 2 deletions download/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
from .downloadstate import DownloadState
from .globalstate import GlobalState
from .messages import download_messages
from .single import download_single_post
from .posts import download_posts
from .timeline import download_timeline


__all__ = [
'download_collections',
'print_download_info',
'download_messages',
'download_single_post',
'download_posts',
'download_timeline',
'DownloadState',
'GlobalState',
Expand Down
121 changes: 121 additions & 0 deletions download/posts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Posts Downloading"""


from .common import get_unique_media_ids, process_download_accessible_media
from .core import DownloadState
from .media import download_media_infos
from .types import DownloadType

from config import FanslyConfig
from fileio.dedupe import dedupe_init
from textio import input_enter_continue, print_error, print_info, print_warning
from utils.common import is_valid_post_id, get_post_id_from_request


def download_posts(config: FanslyConfig, state: DownloadState):
"""Downloads all desired posts."""

# This is important for directory creation later on.
state.download_type = DownloadType.POSTS

print_info(f"You have launched in Posts download mode.")

if config.post_ids is not None:
print_info(f"Trying to download posts {config.post_ids} as specified on the command-line ...")
post_ids = config.post_ids

elif not config.interactive:
raise RuntimeError(
'Posts downloading is not supported in non-interactive mode '
'unless post IDs are specified via command-line.'
)

else:
print_info(
f"Please enter the links or the IDs of the posts you would like to download one after another (confirm with <Enter>)."
f"\n{17 * ' '}After you click on a post, the ID will show in your browser's URL bar."
)
print()

post_ids = []
post_id = "dummy"

while len(post_id) > 0:
while True:
requested_post = input(f"\n{17 * ' '}► Post Link or ID: ")
post_id = get_post_id_from_request(requested_post)

if len(post_id) == 0:
print_info("All desired post links or IDs are recorded. Proceeding to download.")
break
elif is_valid_post_id(post_id):
post_ids.append(post_id)
print_info(f"Post {post_id} is recorded. Enter the next one or hit enter to proceed.")
break
else:
print_error(f"The input string '{requested_post}' can not be a valid post link or ID."
f"\n{22*' '}The last few numbers in the url is the post ID"
f"\n{22*' '}Example: 'https://fansly.com/post/1283998432982'"
f"\n{22*' '}In the example, '1283998432982' is the post ID.",
17
)

for post_id in post_ids:

post_response = config.get_api() \
.get_post(post_id)

if post_response.status_code == 200:
# From: "accounts"
creator_username, creator_display_name = None, None

# post object contains: posts, aggregatedPosts, accountMediaBundles, accountMedia, accounts, tips, tipGoals, stories, polls
post_object = post_response.json()['response']

# if access to post content / post contains content
if post_object['accountMediaBundles'] or post_object['accountMedia']:

# parse post creator name
if creator_username is None:
# the post creators reliable accountId
if post_object['accountMediaBundles']:
state.creator_id = post_object['accountMediaBundles'][0]['accountId']
else:
state.creator_id = post_object['accountMedia'][0]['accountId']

creator_display_name, creator_username = next(
(account.get('displayName'), account.get('username'))
for account in post_object.get('accounts', [])
if account.get('id') == state.creator_id
)

# Override the creator's name with the one from the posting.
# Post ID could be from a different creator than specified
# in the config file.
state.creator_name = creator_username

if creator_display_name and creator_username:
print_info(f"Inspecting post {post_id} by {creator_display_name} (@{creator_username})")
else:
print_info(f"Inspecting post {post_id} by {creator_username.capitalize()}")

# Deferred deduplication init because directory may have changed
# depending on post creator (!= configured creator)
dedupe_init(config, state)

all_media_ids = get_unique_media_ids(post_object)
media_infos = download_media_infos(config, all_media_ids)

process_download_accessible_media(config, state, media_infos, post_id)

if state.duplicate_count > 0 and config.show_downloads and not config.show_skipped_downloads:
print_info(
f"Skipped {state.duplicate_count} already downloaded media item{'' if state.duplicate_count == 1 else 's'}."
)

else:
print_warning(f"Could not find any accessible content in post {post_id}.")

else:
print_error(f"Failed to download post {post_id}. Response code: {post_response.status_code}\n{post_response.text}", 20)
input_enter_continue(config.interactive)
Loading