instagram-archiver 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of instagram-archiver might be problematic. Click here for more details.
- instagram_archiver/__init__.py +8 -2
- instagram_archiver/__main__.py +6 -0
- instagram_archiver/client.py +219 -260
- instagram_archiver/constants.py +52 -92
- instagram_archiver/main.py +78 -46
- instagram_archiver/profile_scraper.py +194 -0
- instagram_archiver/py.typed +0 -0
- instagram_archiver/saved_scraper.py +78 -0
- instagram_archiver/typing.py +170 -0
- instagram_archiver/utils.py +98 -74
- instagram_archiver-0.3.0.dist-info/LICENSE.txt +18 -0
- instagram_archiver-0.3.0.dist-info/METADATA +119 -0
- instagram_archiver-0.3.0.dist-info/RECORD +15 -0
- {instagram_archiver-0.2.0.dist-info → instagram_archiver-0.3.0.dist-info}/WHEEL +1 -1
- instagram_archiver-0.3.0.dist-info/entry_points.txt +4 -0
- instagram_archiver/ig_typing.py +0 -117
- instagram_archiver-0.2.0.dist-info/LICENSE.txt +0 -21
- instagram_archiver-0.2.0.dist-info/METADATA +0 -37
- instagram_archiver-0.2.0.dist-info/RECORD +0 -11
- instagram_archiver-0.2.0.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Typing helpers."""
|
|
2
|
+
# ruff: noqa: D101
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, NotRequired, TypedDict
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
__all__ = ('BrowserName', 'CarouselMedia', 'Comments', 'Edge', 'HasID', 'HighlightsTray',
|
|
11
|
+
'MediaInfo', 'MediaInfoItem', 'MediaInfoItemImageVersions2Candidate', 'UserInfo',
|
|
12
|
+
'WebProfileInfo', 'WebProfileInfoData', 'XDTAPIV1FeedUserTimelineGraphQLConnection',
|
|
13
|
+
'XDTAPIV1FeedUserTimelineGraphQLConnectionContainer', 'XDTMediaDict')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MediaInfoItemVideoVersion(TypedDict):
|
|
17
|
+
height: int
|
|
18
|
+
url: str
|
|
19
|
+
width: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MediaInfoItemImageVersions2Candidate(TypedDict):
|
|
23
|
+
height: int
|
|
24
|
+
"""Height of the image."""
|
|
25
|
+
url: str
|
|
26
|
+
"""URL of the image."""
|
|
27
|
+
width: int
|
|
28
|
+
"""Width of the image."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HighlightItem(TypedDict):
|
|
32
|
+
id: str
|
|
33
|
+
"""Identifier."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HighlightsTray(TypedDict):
|
|
37
|
+
tray: Sequence[HighlightItem]
|
|
38
|
+
"""Highlights tray items."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PageInfo(TypedDict):
|
|
42
|
+
end_cursor: str
|
|
43
|
+
"""End cursor for pagination."""
|
|
44
|
+
has_next_page: bool
|
|
45
|
+
"""Whether there are more pages."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EdgeOwnerToTimelineMedia(TypedDict):
|
|
49
|
+
edges: Sequence[Edge]
|
|
50
|
+
page_info: PageInfo
|
|
51
|
+
"""Pagination information."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UserInfo(TypedDict):
|
|
55
|
+
"""User information."""
|
|
56
|
+
edge_owner_to_timeline_media: EdgeOwnerToTimelineMedia
|
|
57
|
+
"""Timeline media edge."""
|
|
58
|
+
id: str
|
|
59
|
+
"""User ID."""
|
|
60
|
+
profile_pic_url_hd: str
|
|
61
|
+
"""Profile picture URL."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MediaInfoItemImageVersions2(TypedDict):
|
|
65
|
+
candidates: Sequence[MediaInfoItemImageVersions2Candidate]
|
|
66
|
+
"""Image versions."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CarouselMedia(TypedDict):
|
|
70
|
+
image_versions2: MediaInfoItemImageVersions2
|
|
71
|
+
"""Image versions."""
|
|
72
|
+
id: str
|
|
73
|
+
"""Identifier."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class HasID(TypedDict):
|
|
77
|
+
"""Dictionary with an ``id`` field."""
|
|
78
|
+
id: str
|
|
79
|
+
"""Identifier."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MediaInfoItem(TypedDict):
|
|
83
|
+
"""Media information item."""
|
|
84
|
+
carousel_media: NotRequired[Sequence[CarouselMedia] | None]
|
|
85
|
+
"""Carousel media items."""
|
|
86
|
+
image_versions2: MediaInfoItemImageVersions2
|
|
87
|
+
"""Image versions."""
|
|
88
|
+
id: str
|
|
89
|
+
"""Identifier."""
|
|
90
|
+
taken_at: int
|
|
91
|
+
"""Timestamp when the media was taken"""
|
|
92
|
+
user: HasID
|
|
93
|
+
"""User who posted the media."""
|
|
94
|
+
video_dash_manifest: NotRequired[str | None]
|
|
95
|
+
"""URL of the video dash manifest."""
|
|
96
|
+
video_duration: float
|
|
97
|
+
"""Duration of the video in seconds."""
|
|
98
|
+
video_versions: Sequence[MediaInfoItemVideoVersion]
|
|
99
|
+
"""Video versions."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Comments(TypedDict):
|
|
103
|
+
"""Comments container."""
|
|
104
|
+
can_view_more_preview_comments: bool
|
|
105
|
+
"""Whether more preview comments can be viewed."""
|
|
106
|
+
comments: Sequence[HasID]
|
|
107
|
+
"""List of comments."""
|
|
108
|
+
next_min_id: str
|
|
109
|
+
"""Next minimum ID for pagination."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MediaInfo(TypedDict):
|
|
113
|
+
"""Media information."""
|
|
114
|
+
items: Sequence[MediaInfoItem]
|
|
115
|
+
"""List of media items."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Owner(TypedDict):
|
|
119
|
+
id: str
|
|
120
|
+
"""Owner ID."""
|
|
121
|
+
username: str
|
|
122
|
+
"""Owner username."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class XDTMediaDict(TypedDict):
|
|
126
|
+
__typename: Literal['XDTMediaDict']
|
|
127
|
+
"""Type name."""
|
|
128
|
+
code: str
|
|
129
|
+
"""Short code."""
|
|
130
|
+
id: str
|
|
131
|
+
"""Media ID."""
|
|
132
|
+
owner: Owner
|
|
133
|
+
"""Owner information."""
|
|
134
|
+
video_dash_manifest: NotRequired[str | None]
|
|
135
|
+
"""Video dash manifest URL, if available."""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Edge(TypedDict):
|
|
139
|
+
"""Edge of a graph."""
|
|
140
|
+
node: XDTMediaDict
|
|
141
|
+
"""Node at this edge."""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class XDTAPIV1FeedUserTimelineGraphQLConnection(TypedDict):
|
|
145
|
+
edges: Sequence[Edge]
|
|
146
|
+
"""Edges of the graph."""
|
|
147
|
+
page_info: PageInfo
|
|
148
|
+
"""Pagination information."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class XDTAPIV1FeedUserTimelineGraphQLConnectionContainer(TypedDict):
|
|
152
|
+
"""Container for :py:class:`XDTAPIV1FeedUserTimelineGraphQLConnection`."""
|
|
153
|
+
xdt_api__v1__feed__user_timeline_graphql_connection: XDTAPIV1FeedUserTimelineGraphQLConnection
|
|
154
|
+
"""User timeline data."""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class WebProfileInfoData(TypedDict):
|
|
158
|
+
user: UserInfo
|
|
159
|
+
"""User information."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class WebProfileInfo(TypedDict):
|
|
163
|
+
"""Profile information container."""
|
|
164
|
+
data: WebProfileInfoData
|
|
165
|
+
"""Profile data."""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
BrowserName = Literal['brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera', 'safari',
|
|
169
|
+
'vivaldi']
|
|
170
|
+
"""Possible browser choices to get cookies from."""
|
instagram_archiver/utils.py
CHANGED
|
@@ -1,55 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
|
|
1
|
+
"""Utility functions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from
|
|
6
|
-
from typing import Generic, Iterator, Literal, TypeVar
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, override
|
|
7
6
|
import json
|
|
8
7
|
import logging
|
|
9
|
-
import
|
|
8
|
+
import logging.config
|
|
10
9
|
|
|
11
|
-
from loguru import logger
|
|
12
10
|
import click
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .typing import Edge
|
|
14
|
+
|
|
15
|
+
__all__ = ('JSONFormattedString', 'UnknownMimetypeError', 'get_extension', 'json_dumps_formatted',
|
|
16
|
+
'setup_logging', 'write_if_new')
|
|
15
17
|
|
|
16
18
|
T = TypeVar('T')
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
def setup_logging(*,
|
|
22
|
+
debug: bool = False,
|
|
23
|
+
force_color: bool = False,
|
|
24
|
+
no_color: bool = False) -> None: # pragma: no cover
|
|
25
|
+
"""Set up logging configuration."""
|
|
26
|
+
logging.config.dictConfig({
|
|
27
|
+
'version': 1,
|
|
28
|
+
'disable_existing_loggers': True,
|
|
29
|
+
'root': {
|
|
30
|
+
'handlers': ('console',),
|
|
31
|
+
'level': 'DEBUG' if debug else 'INFO',
|
|
32
|
+
},
|
|
33
|
+
'formatters': {
|
|
34
|
+
'default': {
|
|
35
|
+
'()': 'colorlog.ColoredFormatter',
|
|
36
|
+
'force_color': force_color,
|
|
37
|
+
'format':
|
|
38
|
+
'%(log_color)s%(levelname)-8s%(reset)s | %(light_green)s%(name)s%(reset)s:'
|
|
39
|
+
'%(light_red)s%(funcName)s%(reset)s:%(blue)s%(lineno)d%(reset)s - %(message)s',
|
|
40
|
+
'no_color': no_color,
|
|
41
|
+
},
|
|
42
|
+
'simple': {
|
|
43
|
+
'format': '%(message)s',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
'handlers': {
|
|
47
|
+
'console': {
|
|
48
|
+
'class': 'colorlog.StreamHandler',
|
|
49
|
+
'formatter': 'default' if debug else 'simple',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
'loggers': {
|
|
53
|
+
'instagram_archiver': {
|
|
54
|
+
'handlers': ('console',),
|
|
55
|
+
'propagate': False,
|
|
56
|
+
},
|
|
57
|
+
'urllib3': {
|
|
58
|
+
'handlers': ('console',),
|
|
59
|
+
'propagate': False,
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class JSONFormattedString:
|
|
66
|
+
"""Contains a formatted version of the JSON str and the original value."""
|
|
67
|
+
def __init__(self, formatted: str, original: Any) -> None:
|
|
21
68
|
self.formatted = formatted
|
|
69
|
+
"""Formatted JSON string."""
|
|
22
70
|
self.original_value = original
|
|
71
|
+
"""Original value."""
|
|
23
72
|
|
|
73
|
+
@override
|
|
24
74
|
def __str__(self) -> str:
|
|
25
75
|
return self.formatted
|
|
26
76
|
|
|
27
77
|
|
|
28
|
-
def json_dumps_formatted(obj:
|
|
29
|
-
|
|
78
|
+
def json_dumps_formatted(obj: Any) -> JSONFormattedString:
|
|
79
|
+
"""
|
|
80
|
+
Return a special object with the formatted version of the JSON str and the original.
|
|
30
81
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
yield
|
|
38
|
-
finally:
|
|
39
|
-
chdir(old_path)
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
obj : Any
|
|
85
|
+
The object to be formatted.
|
|
86
|
+
"""
|
|
87
|
+
return JSONFormattedString(json.dumps(obj, sort_keys=True, indent=2), obj)
|
|
40
88
|
|
|
41
89
|
|
|
42
90
|
def write_if_new(target: Path | str, content: str | bytes, mode: str = 'w') -> None:
|
|
43
|
-
if
|
|
91
|
+
"""Write a file only if it will be a new file."""
|
|
92
|
+
if not Path(target).is_file():
|
|
44
93
|
with click.open_file(str(target), mode) as f:
|
|
45
94
|
f.write(content)
|
|
46
95
|
|
|
47
96
|
|
|
48
97
|
class UnknownMimetypeError(Exception):
|
|
49
|
-
|
|
98
|
+
"""Raised when an unknown mimetype is encountered in :py:func:`~get_extension`."""
|
|
50
99
|
|
|
51
100
|
|
|
52
101
|
def get_extension(mimetype: str) -> Literal['png', 'jpg']:
|
|
102
|
+
"""
|
|
103
|
+
Get the appropriate three-letter extension for a mimetype.
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
UnknownMimetypeError
|
|
108
|
+
If the mimetype is not recognised.
|
|
109
|
+
"""
|
|
53
110
|
if mimetype == 'image/jpeg':
|
|
54
111
|
return 'jpg'
|
|
55
112
|
if mimetype == 'image/png':
|
|
@@ -57,54 +114,21 @@ def get_extension(mimetype: str) -> Literal['png', 'jpg']:
|
|
|
57
114
|
raise UnknownMimetypeError(mimetype)
|
|
58
115
|
|
|
59
116
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def setup_log_intercept_handler() -> None: # pragma: no cover
|
|
79
|
-
"""Sets up Loguru to intercept records from the logging module."""
|
|
80
|
-
logging.basicConfig(handlers=(InterceptHandler(),), level=0)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def setup_logging(debug: bool | None = False) -> None:
|
|
84
|
-
"""Shared function to enable logging."""
|
|
85
|
-
if debug: # pragma: no cover
|
|
86
|
-
setup_log_intercept_handler()
|
|
87
|
-
logger.enable('')
|
|
88
|
-
else:
|
|
89
|
-
logger.configure(handlers=(dict(
|
|
90
|
-
format='<level>{message}</level>',
|
|
91
|
-
level='INFO',
|
|
92
|
-
sink=sys.stderr,
|
|
93
|
-
),))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class YoutubeDLLogger:
|
|
97
|
-
def debug(self, message: str) -> None:
|
|
98
|
-
if message.startswith('[debug] '):
|
|
99
|
-
logger.debug(message)
|
|
100
|
-
else:
|
|
101
|
-
logger.info(message)
|
|
102
|
-
|
|
103
|
-
def info(self, message: str) -> None:
|
|
104
|
-
pass
|
|
105
|
-
|
|
106
|
-
def warning(self, message: str) -> None:
|
|
107
|
-
logger.warning(message)
|
|
108
|
-
|
|
109
|
-
def error(self, message: str) -> None:
|
|
110
|
-
logger.error(message)
|
|
117
|
+
if TYPE_CHECKING:
|
|
118
|
+
|
|
119
|
+
class InstagramClientInterface(Protocol):
|
|
120
|
+
should_save_comments: bool
|
|
121
|
+
|
|
122
|
+
def save_comments(self, edge: Edge) -> None:
|
|
123
|
+
...
|
|
124
|
+
else:
|
|
125
|
+
InstagramClientInterface = object
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SaveCommentsCheckDisabledMixin(InstagramClientInterface):
|
|
129
|
+
"""Mixin to control saving comments."""
|
|
130
|
+
@override
|
|
131
|
+
def save_comments(self, edge: Edge) -> None:
|
|
132
|
+
if not self.should_save_comments:
|
|
133
|
+
return
|
|
134
|
+
super().save_comments(edge) # type: ignore[safe-super]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 instagram-archiver authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
7
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
15
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
|
18
|
+
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: instagram-archiver
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Save Instagram content you have access to.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: command line,instagram
|
|
7
|
+
Author: Andrew Udvare
|
|
8
|
+
Author-email: audvare@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,<3.14
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: beautifulsoup4 (>=4.13.4,<5.0.0)
|
|
18
|
+
Requires-Dist: click (>=8.2.0,<9.0.0)
|
|
19
|
+
Requires-Dist: colorlog (>=6.9.0,<7.0.0)
|
|
20
|
+
Requires-Dist: html5lib (>=1.1,<2.0)
|
|
21
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
22
|
+
Requires-Dist: typing-extensions (>=4.13.1,<5.0.0)
|
|
23
|
+
Requires-Dist: yt-dlp-utils (>=0,<1)
|
|
24
|
+
Project-URL: Documentation, https://instagram-archiver.readthedocs.org
|
|
25
|
+
Project-URL: Homepage, https://tatsh.github.io/instagram-archiver/
|
|
26
|
+
Project-URL: Issues, https://github.com/Tatsh/instagram-archiver/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/Tatsh/instagram-archiver
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# instagram-archiver
|
|
31
|
+
|
|
32
|
+
[](https://www.python.org/)
|
|
33
|
+
[](https://pypi.org/project/instagram-archiver/)
|
|
34
|
+
[](https://github.com/Tatsh/instagram-archiver/tags)
|
|
35
|
+
[](https://github.com/Tatsh/instagram-archiver/blob/master/LICENSE.txt)
|
|
36
|
+
[](https://github.com/Tatsh/instagram-archiver/compare/v0.3.0...master)
|
|
37
|
+
[](https://github.com/Tatsh/instagram-archiver/actions/workflows/qa.yml)
|
|
38
|
+
[](https://github.com/Tatsh/instagram-archiver/actions/workflows/tests.yml)
|
|
39
|
+
[](https://coveralls.io/github/Tatsh/instagram-archiver?branch=master)
|
|
40
|
+
[](https://instagram-archiver.readthedocs.org/?badge=latest)
|
|
41
|
+
[](http://mypy-lang.org/)
|
|
42
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
43
|
+
[](http://www.pydocstyle.org/en/stable/)
|
|
44
|
+
[](https://docs.pytest.org/en/stable/)
|
|
45
|
+
[](https://github.com/astral-sh/ruff)
|
|
46
|
+
[](https://pepy.tech/project/instagram-archiver)
|
|
47
|
+
[](https://github.com/Tatsh/instagram-archiver/stargazers)
|
|
48
|
+
|
|
49
|
+
[](https://bsky.app/profile/Tatsh.bsky.social)
|
|
50
|
+
[](https://hostux.social/@Tatsh)
|
|
51
|
+
|
|
52
|
+
Save Instagram content you have access to.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
### Poetry
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
poetry add instagram-archiver
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Pip
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
pip install instagram-archiver
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```plain
|
|
71
|
+
Usage: instagram-archiver [OPTIONS] USERNAME
|
|
72
|
+
|
|
73
|
+
Archive a profile's posts.
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
-o, --output-dir DIRECTORY Output directory.
|
|
77
|
+
-b, --browser [brave|chrome|chromium|edge|opera|vivaldi|firefox|safari]
|
|
78
|
+
Browser to read cookies from.
|
|
79
|
+
-p, --profile TEXT Browser profile.
|
|
80
|
+
-d, --debug Enable debug output.
|
|
81
|
+
--no-log Ignore log (re-fetch everything).
|
|
82
|
+
-C, --include-comments Also download all comments (extends download
|
|
83
|
+
time significantly).
|
|
84
|
+
-h, --help Show this message and exit.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Typical use:
|
|
88
|
+
|
|
89
|
+
```shell
|
|
90
|
+
instagram-archiver -o ~/instagram-backups/username username
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `instagram-save-saved`
|
|
94
|
+
|
|
95
|
+
This tool saves your saved posts (at `www.instagram.com/username/saved/all-posts`).
|
|
96
|
+
|
|
97
|
+
```plain
|
|
98
|
+
Usage: instagram-save-saved [OPTIONS]
|
|
99
|
+
|
|
100
|
+
Archive your saved posts.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
-o, --output-dir DIRECTORY Output directory.
|
|
104
|
+
-b, --browser [brave|chrome|chromium|edge|opera|vivaldi|firefox|safari]
|
|
105
|
+
Browser to read cookies from.
|
|
106
|
+
-p, --profile TEXT Browser profile.
|
|
107
|
+
-d, --debug Enable debug output.
|
|
108
|
+
-C, --include-comments Also download all comments (extends download
|
|
109
|
+
time significantly).
|
|
110
|
+
-u, --unsave Unsave posts after successful archive.
|
|
111
|
+
-h, --help Show this message and exit.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Notes
|
|
115
|
+
|
|
116
|
+
The default output path is the username under the current working directory.
|
|
117
|
+
|
|
118
|
+
Videos are saved using yt-dlp and its respective configuration.
|
|
119
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
instagram_archiver/__init__.py,sha256=406v5g-tu6QhUfdMTNgI9rYbPYm7WfjiIL68HOXLXQY,270
|
|
2
|
+
instagram_archiver/__main__.py,sha256=oQ6s6zvZTBiEOgt-qep3bDY9ayxSanQr7KHzr6ENK0o,115
|
|
3
|
+
instagram_archiver/client.py,sha256=uMYembbrYWKJdXlBpuYsSK-p7tuj0qG3WMVV8M785NE,11326
|
|
4
|
+
instagram_archiver/constants.py,sha256=NJ8QlQZviY3dwwrIONThK_G9VcvAzOQM6Yg-hSyaj9A,1459
|
|
5
|
+
instagram_archiver/main.py,sha256=lW8rHPjQpqNH8TqjitW3DMILazn09sSWxJhVlkUC5Ck,3808
|
|
6
|
+
instagram_archiver/profile_scraper.py,sha256=BGnJZD3rF2e4aHpqSvG7UBoAiDjTyrpcYKSzsDgHO3M,7988
|
|
7
|
+
instagram_archiver/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
instagram_archiver/saved_scraper.py,sha256=PHSXkmK-MC-42Z4GZzNCXa8dSD682NEav1rms7mL-yk,2722
|
|
9
|
+
instagram_archiver/typing.py,sha256=J8TcQftpIY-IVsbx007e1WbA-XdLUhBuEWWAisFlpHA,4306
|
|
10
|
+
instagram_archiver/utils.py,sha256=l6f0W_brZhVPOjlKwoFYYum7ICyHJXpboTU7ANIQSPI,3842
|
|
11
|
+
instagram_archiver-0.3.0.dist-info/LICENSE.txt,sha256=cDLmbhzFwEUz5FL_OnA6Jp9zdz80330J6YyEq-00yNQ,1093
|
|
12
|
+
instagram_archiver-0.3.0.dist-info/METADATA,sha256=Vq8yWfCHOzOJduL9E0PEcIGz9qu900uiI96c1g0q3-I,5925
|
|
13
|
+
instagram_archiver-0.3.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
14
|
+
instagram_archiver-0.3.0.dist-info/entry_points.txt,sha256=kNXd0Sy6896DEBRcx2mVYiaE-OR9-XR56MpWuaNa49g,128
|
|
15
|
+
instagram_archiver-0.3.0.dist-info/RECORD,,
|
instagram_archiver/ig_typing.py
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# pylint: disable=unused-private-member
|
|
2
|
-
from typing import Literal, Sequence, TypedDict
|
|
3
|
-
|
|
4
|
-
from typing_extensions import NotRequired
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class MediaInfoItemVideoVersion(TypedDict):
|
|
8
|
-
height: int
|
|
9
|
-
url: str
|
|
10
|
-
width: int
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MediaInfoItemImageVersions2Candidate(TypedDict):
|
|
14
|
-
height: int
|
|
15
|
-
url: str
|
|
16
|
-
width: int
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class HighlightItem(TypedDict):
|
|
20
|
-
id: str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class HighlightsTray(TypedDict):
|
|
24
|
-
tray: Sequence[HighlightItem]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class EdgeOwnerToTimelineMediaPageInfo(TypedDict):
|
|
28
|
-
end_cursor: str
|
|
29
|
-
has_next_page: bool
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class EdgeOwnerToTimelineMedia(TypedDict):
|
|
33
|
-
edges: Sequence['Edge']
|
|
34
|
-
page_info: EdgeOwnerToTimelineMediaPageInfo
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class UserInfo(TypedDict):
|
|
38
|
-
edge_owner_to_timeline_media: EdgeOwnerToTimelineMedia
|
|
39
|
-
id: str
|
|
40
|
-
profile_pic_url_hd: str
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class MediaInfoItemImageVersions2(TypedDict):
|
|
44
|
-
candidates: Sequence[MediaInfoItemImageVersions2Candidate]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class CarouselMedia(TypedDict):
|
|
48
|
-
image_versions2: MediaInfoItemImageVersions2
|
|
49
|
-
id: str
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class HasID(TypedDict):
|
|
53
|
-
id: str
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class MediaInfoItem(TypedDict):
|
|
57
|
-
carousel_media: NotRequired[list[CarouselMedia]]
|
|
58
|
-
image_versions2: MediaInfoItemImageVersions2
|
|
59
|
-
id: str
|
|
60
|
-
taken_at: int
|
|
61
|
-
user: HasID
|
|
62
|
-
video_dash_manifest: str
|
|
63
|
-
video_duration: float
|
|
64
|
-
video_versions: Sequence[MediaInfoItemVideoVersion]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class EdgeSidecarToChildren(TypedDict):
|
|
68
|
-
edges: Sequence['Edge']
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class EdgeMediaToComment(TypedDict):
|
|
72
|
-
count: int
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class Comments(TypedDict):
|
|
76
|
-
can_view_more_preview_comments: bool
|
|
77
|
-
comments: list[HasID]
|
|
78
|
-
next_min_id: str
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class MediaInfo(TypedDict):
|
|
82
|
-
more_available: bool
|
|
83
|
-
num_results: int
|
|
84
|
-
items: Sequence[MediaInfoItem]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class GraphSidecarNode(TypedDict):
|
|
88
|
-
__typename: Literal['GraphSidecar']
|
|
89
|
-
comments_disabled: bool
|
|
90
|
-
edge_media_to_comment: EdgeMediaToComment
|
|
91
|
-
edge_sidecar_to_children: EdgeSidecarToChildren
|
|
92
|
-
id: str
|
|
93
|
-
shortcode: NotRequired[str]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class GraphVideoNode(TypedDict):
|
|
97
|
-
__typename: Literal['GraphVideo']
|
|
98
|
-
id: str
|
|
99
|
-
shortcode: NotRequired[str]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class GraphImageNode(TypedDict):
|
|
103
|
-
__typename: Literal['GraphImage']
|
|
104
|
-
id: str
|
|
105
|
-
shortcode: NotRequired[str]
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class Edge(TypedDict):
|
|
109
|
-
node: GraphSidecarNode | GraphImageNode | GraphVideoNode
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class WebProfileInfoData(TypedDict):
|
|
113
|
-
user: UserInfo
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class WebProfileInfo(TypedDict):
|
|
117
|
-
data: WebProfileInfoData
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 Andrew Udvare
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|