lyrics-transcriber 0.30.0__py3-none-any.whl → 0.32.1__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.
- lyrics_transcriber/__init__.py +2 -1
- lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +164 -166
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +256 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +30 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
- lyrics_transcriber/correction/handlers/repeat.py +71 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
- lyrics_transcriber/correction/handlers/word_operations.py +135 -0
- lyrics_transcriber/correction/phrase_analyzer.py +426 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
- lyrics_transcriber/lyrics/genius.py +73 -0
- lyrics_transcriber/lyrics/spotify.py +82 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +37 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +219 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +503 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +140 -171
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +91 -0
- lyrics_transcriber/output/segment_resizer.py +416 -0
- lyrics_transcriber/output/subtitles.py +328 -302
- lyrics_transcriber/output/video.py +219 -0
- lyrics_transcriber/review/__init__.py +1 -0
- lyrics_transcriber/review/server.py +138 -0
- lyrics_transcriber/storage/dropbox.py +110 -134
- lyrics_transcriber/transcribers/audioshake.py +171 -105
- lyrics_transcriber/transcribers/base_transcriber.py +149 -0
- lyrics_transcriber/transcribers/whisper.py +267 -133
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
- lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
- lyrics_transcriber/core/corrector.py +0 -56
- lyrics_transcriber/core/fetcher.py +0 -143
- lyrics_transcriber/storage/tokens.py +0 -116
- lyrics_transcriber/transcribers/base.py +0 -31
- lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
- lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/LICENSE +0 -0
@@ -1,249 +1,225 @@
|
|
1
|
-
import
|
2
|
-
from
|
3
|
-
from dropbox.files import WriteMode
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Protocol, BinaryIO, Optional, List, Any
|
4
3
|
import os
|
5
4
|
import time
|
6
5
|
import logging
|
7
6
|
import requests
|
7
|
+
from dropbox import Dropbox
|
8
|
+
from dropbox.files import WriteMode, FileMetadata
|
9
|
+
from dropbox.sharing import RequestedVisibility, SharedLinkSettings
|
10
|
+
from dropbox.exceptions import AuthError, ApiError
|
8
11
|
|
9
12
|
logger = logging.getLogger(__name__)
|
10
13
|
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
self.app_secret = app_secret or os.environ.get("WHISPER_DROPBOX_APP_SECRET")
|
16
|
-
self.refresh_token = refresh_token or os.environ.get("WHISPER_DROPBOX_REFRESH_TOKEN")
|
17
|
-
self.access_token = access_token or os.environ.get("WHISPER_DROPBOX_ACCESS_TOKEN")
|
18
|
-
self.dbx = Dropbox(self.access_token)
|
19
|
-
|
20
|
-
def _refresh_access_token(self):
|
21
|
-
"""Refresh the access token using the refresh token."""
|
22
|
-
try:
|
23
|
-
logger.debug("Attempting to refresh access token")
|
24
|
-
# Prepare the token refresh request
|
25
|
-
data = {"grant_type": "refresh_token", "refresh_token": self.refresh_token}
|
26
|
-
auth = (self.app_key, self.app_secret)
|
27
|
-
|
28
|
-
logger.debug(f"Making refresh token request to Dropbox API")
|
29
|
-
response = requests.post("https://api.dropbox.com/oauth2/token", data=data, auth=auth)
|
30
|
-
|
31
|
-
logger.debug(f"Received response from Dropbox API. Status code: {response.status_code}")
|
32
|
-
if response.status_code == 200:
|
33
|
-
result = response.json()
|
34
|
-
self.access_token = result["access_token"]
|
35
|
-
self.dbx = Dropbox(self.access_token)
|
36
|
-
logger.info("Successfully refreshed access token")
|
37
|
-
else:
|
38
|
-
logger.error(f"Failed to refresh token. Status code: {response.status_code}, Response: {response.text}")
|
15
|
+
@dataclass
|
16
|
+
class DropboxConfig:
|
17
|
+
"""Configuration for Dropbox client."""
|
39
18
|
|
40
|
-
|
41
|
-
|
42
|
-
|
19
|
+
app_key: Optional[str] = None
|
20
|
+
app_secret: Optional[str] = None
|
21
|
+
refresh_token: Optional[str] = None
|
43
22
|
|
44
|
-
@
|
45
|
-
def
|
46
|
-
"""
|
23
|
+
@classmethod
|
24
|
+
def from_env(cls) -> "DropboxConfig":
|
25
|
+
"""Create config from environment variables."""
|
26
|
+
return cls(
|
27
|
+
app_key=os.environ.get("WHISPER_DROPBOX_APP_KEY"),
|
28
|
+
app_secret=os.environ.get("WHISPER_DROPBOX_APP_SECRET"),
|
29
|
+
refresh_token=os.environ.get("WHISPER_DROPBOX_REFRESH_TOKEN"),
|
30
|
+
)
|
47
31
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
def
|
32
|
+
|
33
|
+
class DropboxAPI(Protocol):
|
34
|
+
"""Protocol for Dropbox API operations."""
|
35
|
+
|
36
|
+
def files_upload(self, f: bytes, path: str, mode: WriteMode) -> Any: ...
|
37
|
+
def files_list_folder(self, path: str, recursive: bool = False) -> Any: ...
|
38
|
+
def files_list_folder_continue(self, cursor: str) -> Any: ...
|
39
|
+
def files_download(self, path: str) -> tuple[Any, Any]: ...
|
40
|
+
def files_download_to_file(self, download_path: str, path: str) -> None: ...
|
41
|
+
def files_get_metadata(self, path: str) -> Any: ...
|
42
|
+
def sharing_create_shared_link_with_settings(self, path: str, settings: SharedLinkSettings) -> Any: ...
|
43
|
+
def sharing_list_shared_links(self, path: str) -> Any: ...
|
44
|
+
|
45
|
+
|
46
|
+
class DropboxHandler:
|
47
|
+
"""Handles Dropbox storage operations with automatic token refresh."""
|
48
|
+
|
49
|
+
def __init__(
|
50
|
+
self,
|
51
|
+
config: Optional[DropboxConfig] = None,
|
52
|
+
client: Optional[DropboxAPI] = None,
|
53
|
+
):
|
54
|
+
"""Initialize the Dropbox handler."""
|
55
|
+
self.config = config or DropboxConfig.from_env()
|
56
|
+
self._validate_config()
|
57
|
+
|
58
|
+
self.client = client or Dropbox(
|
59
|
+
app_key=self.config.app_key,
|
60
|
+
app_secret=self.config.app_secret,
|
61
|
+
oauth2_refresh_token=self.config.refresh_token,
|
62
|
+
)
|
63
|
+
|
64
|
+
def _validate_config(self) -> None:
|
65
|
+
"""Validate the configuration."""
|
66
|
+
logger.debug("Validating DropboxConfig with values:")
|
67
|
+
logger.debug(f"app_key: {self.config.app_key[:4] + '...' if self.config.app_key else 'None'}")
|
68
|
+
logger.debug(f"app_secret: {self.config.app_secret[:4] + '...' if self.config.app_secret else 'None'}")
|
69
|
+
logger.debug(f"refresh_token: {self.config.refresh_token[:4] + '...' if self.config.refresh_token else 'None'}")
|
70
|
+
|
71
|
+
missing = []
|
72
|
+
if not self.config.app_key:
|
73
|
+
missing.append("app_key")
|
74
|
+
if not self.config.app_secret:
|
75
|
+
missing.append("app_secret")
|
76
|
+
if not self.config.refresh_token:
|
77
|
+
missing.append("refresh_token")
|
78
|
+
|
79
|
+
if missing:
|
80
|
+
error_msg = f"Missing required Dropbox configuration: {', '.join(missing)}"
|
81
|
+
logger.error(error_msg)
|
82
|
+
raise ValueError(error_msg)
|
83
|
+
|
84
|
+
def upload_with_retry(self, file: BinaryIO, path: str, max_retries: int = 3) -> None:
|
66
85
|
"""Upload a file to Dropbox with retries."""
|
67
86
|
for attempt in range(max_retries):
|
68
87
|
try:
|
69
88
|
logger.debug(f"Attempting file upload to {path} (attempt {attempt + 1}/{max_retries})")
|
70
89
|
file.seek(0)
|
71
|
-
self.
|
90
|
+
self.client.files_upload(file.read(), path, mode=WriteMode.overwrite)
|
72
91
|
logger.debug(f"Successfully uploaded file to {path}")
|
73
92
|
return
|
74
|
-
except
|
93
|
+
except ApiError as e:
|
75
94
|
logger.warning(f"Upload attempt {attempt + 1} failed: {str(e)}")
|
76
95
|
if attempt == max_retries - 1:
|
77
|
-
logger.error(f"All upload attempts failed for {path}
|
96
|
+
logger.error(f"All upload attempts failed for {path}")
|
78
97
|
raise
|
79
|
-
|
80
|
-
logger.debug(f"Waiting {sleep_time} seconds before retry")
|
81
|
-
time.sleep(sleep_time)
|
98
|
+
time.sleep(1 * (attempt + 1))
|
82
99
|
|
83
|
-
|
84
|
-
def upload_string_with_retry(self, content, path, max_retries=3):
|
100
|
+
def upload_string_with_retry(self, content: str, path: str, max_retries: int = 3) -> None:
|
85
101
|
"""Upload a string content to Dropbox with retries."""
|
86
102
|
for attempt in range(max_retries):
|
87
103
|
try:
|
88
104
|
logger.debug(f"Attempting string upload to {path} (attempt {attempt + 1}/{max_retries})")
|
89
|
-
self.
|
105
|
+
self.client.files_upload(content.encode(), path, mode=WriteMode.overwrite)
|
90
106
|
logger.debug(f"Successfully uploaded string content to {path}")
|
91
107
|
return
|
92
|
-
except
|
108
|
+
except ApiError as e:
|
93
109
|
logger.warning(f"Upload attempt {attempt + 1} failed: {str(e)}")
|
94
110
|
if attempt == max_retries - 1:
|
95
|
-
logger.error(f"All upload attempts failed for {path}
|
111
|
+
logger.error(f"All upload attempts failed for {path}")
|
96
112
|
raise
|
97
|
-
|
98
|
-
logger.debug(f"Waiting {sleep_time} seconds before retry")
|
99
|
-
time.sleep(sleep_time)
|
113
|
+
time.sleep(1 * (attempt + 1))
|
100
114
|
|
101
|
-
|
102
|
-
def list_folder_recursive(self, path=""):
|
115
|
+
def list_folder_recursive(self, path: str = "") -> List[FileMetadata]:
|
103
116
|
"""List all files in a folder recursively."""
|
104
117
|
try:
|
105
118
|
logger.debug(f"Listing files recursively from {path}")
|
106
119
|
entries = []
|
107
|
-
result = self.
|
120
|
+
result = self.client.files_list_folder(path, recursive=True)
|
108
121
|
|
109
122
|
while True:
|
110
123
|
entries.extend(result.entries)
|
111
124
|
if not result.has_more:
|
112
125
|
break
|
113
|
-
|
114
|
-
result = self.dbx.files_list_folder_continue(result.cursor)
|
126
|
+
result = self.client.files_list_folder_continue(result.cursor)
|
115
127
|
|
116
128
|
return entries
|
117
|
-
|
118
|
-
except (dropbox.exceptions.AuthError, dropbox.exceptions.ApiError):
|
119
|
-
# Let the decorator handle these
|
120
|
-
raise
|
121
129
|
except Exception as e:
|
122
|
-
logger.error(f"Error listing files
|
130
|
+
logger.error(f"Error listing files: {str(e)}", exc_info=True)
|
123
131
|
raise
|
124
132
|
|
125
|
-
|
126
|
-
def download_file_content(self, path):
|
133
|
+
def download_file_content(self, path: str) -> bytes:
|
127
134
|
"""Download and return the content of a file."""
|
128
135
|
try:
|
129
136
|
logger.debug(f"Downloading file content from {path}")
|
130
|
-
return self.
|
137
|
+
return self.client.files_download(path)[1].content
|
131
138
|
except Exception as e:
|
132
|
-
logger.error(f"Error downloading file
|
139
|
+
logger.error(f"Error downloading file: {str(e)}", exc_info=True)
|
133
140
|
raise
|
134
141
|
|
135
|
-
|
136
|
-
def download_folder(self, dropbox_path, local_path):
|
142
|
+
def download_folder(self, dropbox_path: str, local_path: str) -> None:
|
137
143
|
"""Download all files from a Dropbox folder to a local path."""
|
138
144
|
try:
|
139
145
|
logger.debug(f"Downloading folder {dropbox_path} to {local_path}")
|
146
|
+
entries = self.list_folder_recursive(dropbox_path)
|
140
147
|
|
141
|
-
# List all files in the folder
|
142
|
-
result = self.dbx.files_list_folder(dropbox_path, recursive=True)
|
143
|
-
entries = result.entries
|
144
|
-
|
145
|
-
# Continue fetching if there are more files
|
146
|
-
while result.has_more:
|
147
|
-
result = self.dbx.files_list_folder_continue(result.cursor)
|
148
|
-
entries.extend(result.entries)
|
149
|
-
|
150
|
-
# Download each file
|
151
148
|
for entry in entries:
|
152
|
-
if isinstance(entry,
|
153
|
-
# Calculate relative path from the root folder
|
149
|
+
if isinstance(entry, FileMetadata):
|
154
150
|
rel_path = entry.path_display[len(dropbox_path) :].lstrip("/")
|
155
151
|
local_file_path = os.path.join(local_path, rel_path)
|
156
152
|
|
157
|
-
# Create directories if they don't exist
|
158
153
|
os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
|
159
|
-
|
160
|
-
# Download the file
|
161
154
|
logger.debug(f"Downloading {entry.path_display} to {local_file_path}")
|
162
|
-
self.
|
163
|
-
|
164
|
-
logger.debug(f"Successfully downloaded folder {dropbox_path} to {local_path}")
|
155
|
+
self.client.files_download_to_file(local_file_path, entry.path_display)
|
165
156
|
|
157
|
+
logger.debug(f"Successfully downloaded folder {dropbox_path}")
|
166
158
|
except Exception as e:
|
167
|
-
logger.error(f"Error downloading folder
|
159
|
+
logger.error(f"Error downloading folder: {str(e)}", exc_info=True)
|
168
160
|
raise
|
169
161
|
|
170
|
-
|
171
|
-
def upload_folder(self, local_path, dropbox_path):
|
162
|
+
def upload_folder(self, local_path: str, dropbox_path: str) -> None:
|
172
163
|
"""Upload all files from a local folder to a Dropbox path."""
|
173
164
|
try:
|
174
165
|
logger.debug(f"Uploading folder {local_path} to {dropbox_path}")
|
175
|
-
|
176
|
-
# Walk through all files in the local folder
|
177
|
-
for root, dirs, files in os.walk(local_path):
|
166
|
+
for root, _, files in os.walk(local_path):
|
178
167
|
for filename in files:
|
179
168
|
local_file_path = os.path.join(root, filename)
|
180
|
-
# Calculate relative path from local_path
|
181
169
|
rel_path = os.path.relpath(local_file_path, local_path)
|
182
170
|
target_path = f"{dropbox_path}/{rel_path}"
|
183
171
|
|
184
172
|
logger.debug(f"Uploading {rel_path} to {target_path}")
|
185
173
|
with open(local_file_path, "rb") as f:
|
186
|
-
self.
|
187
|
-
|
188
|
-
logger.debug(f"Successfully uploaded folder {local_path} to {dropbox_path}")
|
174
|
+
self.client.files_upload(f.read(), target_path, mode=WriteMode.overwrite)
|
189
175
|
|
176
|
+
logger.debug(f"Successfully uploaded folder {local_path}")
|
190
177
|
except Exception as e:
|
191
|
-
logger.error(f"Error uploading folder
|
178
|
+
logger.error(f"Error uploading folder: {str(e)}", exc_info=True)
|
192
179
|
raise
|
193
180
|
|
194
|
-
|
195
|
-
def create_shared_link(self, path):
|
181
|
+
def create_shared_link(self, path: str) -> str:
|
196
182
|
"""Create a shared link for a file that's accessible without login."""
|
197
183
|
try:
|
198
184
|
logger.debug(f"Creating shared link for {path}")
|
199
|
-
shared_link = self.
|
200
|
-
path, settings=
|
185
|
+
shared_link = self.client.sharing_create_shared_link_with_settings(
|
186
|
+
path, settings=SharedLinkSettings(requested_visibility=RequestedVisibility.public)
|
201
187
|
)
|
202
|
-
# Convert dropbox shared link to direct download link
|
203
188
|
return shared_link.url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
|
204
189
|
except Exception as e:
|
205
190
|
logger.error(f"Error creating shared link: {str(e)}", exc_info=True)
|
206
191
|
raise
|
207
192
|
|
208
|
-
|
209
|
-
def get_existing_shared_link(self, path):
|
193
|
+
def get_existing_shared_link(self, path: str) -> Optional[str]:
|
210
194
|
"""Get existing shared link for a file if it exists."""
|
211
195
|
try:
|
212
196
|
logger.debug(f"Getting existing shared link for {path}")
|
213
|
-
shared_links = self.
|
197
|
+
shared_links = self.client.sharing_list_shared_links(path=path).links
|
214
198
|
if shared_links:
|
215
|
-
# Convert to direct download link
|
216
199
|
return shared_links[0].url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
|
217
200
|
return None
|
218
201
|
except Exception as e:
|
219
202
|
logger.error(f"Error getting existing shared link: {str(e)}", exc_info=True)
|
220
203
|
return None
|
221
204
|
|
222
|
-
|
223
|
-
def create_or_get_shared_link(self, path):
|
205
|
+
def create_or_get_shared_link(self, path: str) -> str:
|
224
206
|
"""Create a shared link or get existing one."""
|
225
207
|
try:
|
226
|
-
# First try to get existing link
|
227
208
|
existing_link = self.get_existing_shared_link(path)
|
228
209
|
if existing_link:
|
229
210
|
logger.debug(f"Found existing shared link for {path}")
|
230
211
|
return existing_link
|
231
212
|
|
232
|
-
# If no existing link, create new one
|
233
213
|
logger.debug(f"Creating new shared link for {path}")
|
234
|
-
|
235
|
-
path, settings=dropbox.sharing.SharedLinkSettings(requested_visibility=dropbox.sharing.RequestedVisibility.public)
|
236
|
-
)
|
237
|
-
return shared_link.url.replace("www.dropbox.com", "dl.dropboxusercontent.com")
|
214
|
+
return self.create_shared_link(path)
|
238
215
|
except Exception as e:
|
239
|
-
logger.error(f"Error creating
|
216
|
+
logger.error(f"Error creating/getting shared link: {str(e)}", exc_info=True)
|
240
217
|
raise
|
241
218
|
|
242
|
-
|
243
|
-
def file_exists(self, path):
|
219
|
+
def file_exists(self, path: str) -> bool:
|
244
220
|
"""Check if a file exists in Dropbox."""
|
245
221
|
try:
|
246
|
-
self.
|
222
|
+
self.client.files_get_metadata(path)
|
247
223
|
return True
|
248
224
|
except:
|
249
225
|
return False
|