Skip to content

Commit

Permalink
rename single mode to posts mode, add option to batch download severa…
Browse files Browse the repository at this point in the history
…l posts
  • Loading branch information
1gintonic committed Jun 5, 2024
1 parent 4739155 commit e16c47b
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 139 deletions.
31 changes: 16 additions & 15 deletions config/args.py
Original file line number Diff line number Diff line change
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='POST_ID',
dest='download_mode_single',
help='Use "Single" download mode. This will download a single post '
"by ID from an arbitrary creator. "
nargs='*',
dest='download_mode_posts',
help='Use "Posts" download mode. This will download all desired posts '
"by 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 @@ -375,17 +375,18 @@ def map_args_to_config(args: argparse.Namespace, config: FanslyConfig) -> None:
config.download_mode = DownloadMode.COLLECTION
config_overridden = True

if args.download_mode_single is not None:
post_id = 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. "
"At least 10 characters/only digits required."
)
if args.download_mode_posts is not None:
post_ids = 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. "
"At least 10 characters/only digits required."
)

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

if args.metadata_handling is not None:
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
119 changes: 119 additions & 0 deletions download/posts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""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


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 IDs of the posts you would like to download one after another (confirm with <Enter>)."
f"\n{17*' '}After you click on a post, it will show in your browsers URL bar."
)
print()

post_ids = []
post_id = "dummy"

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

if len(post_id) == 0:
print_info("All desired post 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 '{post_id}' can not be a valid post 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' would be 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 a post by {creator_display_name} (@{creator_username})")
else:
print_info(f"Inspecting a post 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)
109 changes: 0 additions & 109 deletions download/single.py

This file was deleted.

2 changes: 1 addition & 1 deletion download/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class DownloadType(StrEnum):
NOTSET = auto()
COLLECTIONS = auto()
MESSAGES = auto()
SINGLE = auto()
POSTS = auto()
TIMELINE = auto()
8 changes: 4 additions & 4 deletions fansly_downloader_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def main(config: FanslyConfig) -> int:
state = DownloadState(creator_name=creator_name)

# Special treatment for deviating folder names later
if not config.download_mode == DownloadMode.SINGLE:
if not config.download_mode == DownloadMode.POSTS:
dedupe_init(config, state)

print_download_info(config)
Expand All @@ -149,14 +149,14 @@ def main(config: FanslyConfig) -> int:
# Normal: Downloads Timeline + Messages one after another.
# Timeline: Scrapes only the creator's timeline content.
# Messages: Scrapes only the creator's messages content.
# Single: Fetch a single post by the post's ID. Click on a post to see its ID in the url bar e.g. ../post/1283493240234
# Posts: Fetch all desired post by their ID. Click on a post to see its ID in the url bar e.g. ../post/1283493240234
# Collection: Download all content listed within the "Purchased Media Collection"

print_info(f'Download mode is: {config.download_mode_str()}')
print()

if config.download_mode == DownloadMode.SINGLE:
download_single_post(config, state)
if config.download_mode == DownloadMode.POSTS:
download_posts(config, state)

elif config.download_mode == DownloadMode.COLLECTION:
download_collections(config, state)
Expand Down
2 changes: 1 addition & 1 deletion pathio/pathio.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def set_create_directory_for_download(config: FanslyConfig, state: DownloadState
elif state.download_type == DownloadType.TIMELINE and config.separate_timeline:
download_directory = user_base_path / 'Timeline'

elif state.download_type == DownloadType.SINGLE and config.separate_timeline:
elif state.download_type == DownloadType.POSTS and config.separate_timeline:
download_directory = user_base_path / 'Timeline'

# Save state
Expand Down

0 comments on commit e16c47b

Please sign in to comment.