Surn commited on
Commit
4e27a59
·
1 Parent(s): 1312249

Add shortened URL and Open Graph

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +124 -38
  3. modules/constants.py +12 -1
  4. modules/storage.py +170 -1
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 👀
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 5.29.1
8
  python_version: 3.12.8
9
  license: apache-2.0
10
  app_file: app.py
 
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.31.0
8
  python_version: 3.12.8
9
  license: apache-2.0
10
  app_file: app.py
app.py CHANGED
@@ -5,10 +5,45 @@ import modules.constants as constants
5
  import modules.version_info as version_info
6
  import modules.storage as storage
7
 
 
 
8
 
9
  user_dir = constants.TMPDIR
10
  default_folder = "saved_models/3d_model_" + format(random.randint(1, 999999), "06d")
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  def getVersions():
13
  #return html_versions
14
  return version_info.versions_html()
@@ -30,8 +65,6 @@ def process_url(url, default_ext=".png"):
30
 
31
  # Parse URL to get components
32
  try:
33
- import urllib.request
34
- from urllib.parse import urlparse
35
 
36
  # Create filename from URL.
37
  parsed_url = urlparse(url)
@@ -98,7 +131,6 @@ def process_url(url, default_ext=".png"):
98
  print(f"Error downloading file {url}: {e}")
99
  return url # Return original URL if download fails
100
 
101
-
102
  def load_data(request: gr.Request, model_3d, image_slider):
103
  """
104
  Load data from query parameters, download files if needed,
@@ -121,15 +153,19 @@ def load_data(request: gr.Request, model_3d, image_slider):
121
  # Parse query parameters.
122
  query_params = dict(request.query_params) if request is not None else {}
123
 
124
- # Extract URLs from query parameters.
 
 
 
125
  model_url = query_params.get("3d", None)
126
  hm_url = query_params.get("hm", None)
127
  img_url = query_params.get("image", None)
128
 
129
- if model_url is None and hm_url is None and img_url is None:
130
- # No URLs provided, return default values.
131
- query_params = {}
132
-
 
133
  # Process the model URL if provided.
134
  if model_url:
135
  model_url = process_url(model_url, default_ext=".glb")
@@ -143,25 +179,33 @@ def load_data(request: gr.Request, model_3d, image_slider):
143
  if hm_url:
144
  local_hm = process_url(hm_url, default_ext=".png")
145
  if local_hm:
146
- slider_images.append(local_hm)
 
 
 
 
 
147
 
148
 
149
- # Set default values if no URLs provided:
150
- default_model = getattr(model_3d, "value", model_3d)
151
- default_images = getattr(image_slider, "value", image_slider)
152
 
153
  if not slider_images:
154
- slider_images = default_images if not default_images == (None,None) else constants.default_slider_images
155
 
156
  if not model_url:
157
- model_url = default_model if default_model else constants.default_model_3d
158
 
159
- # If any query parameters were provided, generate a permalink.
160
  permalink = ""
161
- if query_params:
162
  try:
163
- # Use the helper function defined in storage.py
164
- permalink = storage.generate_permalink_from_urls(model_url, hm_url, img_url)
 
 
 
165
  except Exception as e:
166
  print(f"Error generating permalink: {e}")
167
 
@@ -212,21 +256,70 @@ def process_upload(files, current_model, current_images):
212
 
213
  # For the image slider, we expect a list of exactly 2 images.
214
  # Start with current images (or use defaults if None).
215
- if current_images is None or not isinstance(current_images, list):
216
  new_images = [None, None]
217
  else:
218
- new_images = current_images + [None] * (2 - len(current_images))
219
- new_images = new_images[:2]
220
-
221
- # If at least one image is uploaded, update the corresponding slot(s).
222
- for i in range(len(extracted_images)):
223
- if i < 2:
224
- new_images[i] = extracted_images[i]
225
-
 
 
226
  return updated_model, new_images
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  gr.set_static_paths(paths=["images/", "models/", "assets/"])
229
- with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/beeuty',delete_cache=(21600,86400), fill_width=True) as viewer3d:
230
  gr.Markdown("# 3D Model Viewer")
231
 
232
  with gr.Row():
@@ -257,7 +350,6 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
257
  )
258
 
259
  with gr.Row():
260
- # New textbox for folder name.
261
  folder_name_box = gr.Textbox(
262
  label="Folder Name",
263
  value=default_folder,
@@ -282,15 +374,12 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
282
  with gr.Row():
283
  gr.HTML(value=getVersions(), visible=True, elem_id="versions")
284
 
285
- # Use JavaScript to pass the query parameters to your callback.
286
  viewer3d.load(
287
  load_data,
288
  inputs=[model_3d, image_slider],
289
  outputs=[model_3d, image_slider, permalink],
290
  scroll_to_output=True
291
  ).then(
292
- # If the returned permalink (link) is non-empty then make the permalink row visible
293
- # and disable the permalink button; otherwise, hide the row and enable the button.
294
  lambda link: (gr.update(visible=True), gr.update(interactive=False))
295
  if link and len(link) > 0
296
  else (gr.update(visible=False), gr.update(interactive=True)),
@@ -298,7 +387,6 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
298
  outputs=[permalink_row, permalink_button]
299
  )
300
 
301
- # Process uploaded files to update the Model3D or ImageSlider component.
302
  upload_btn.upload(
303
  process_upload,
304
  inputs=[upload_btn, model_3d, image_slider],
@@ -307,20 +395,18 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
307
  api_name="process_upload",
308
  show_progress=True
309
  ).then(
310
- # After a successful upload, enable the permalink button.
311
  lambda m, i: gr.update(interactive=True),
312
  inputs=[model_3d, image_slider],
313
  outputs=[permalink_button]
314
  )
315
- # Generate a permalink based on the current model, images, and folder name.
316
  permalink_button.click(
317
  lambda model, images, folder: storage.upload_files_to_repo(
318
- files=[model] + list(images),
319
- repo_id="Surn/Storage",
320
  folder_name=folder,
321
  create_permalink=True,
322
  repo_type="dataset"
323
- )[1], # Extract the permalink from the returned tuple if criteria met.
324
  inputs=[model_3d, image_slider, folder_name_box],
325
  outputs=[permalink],
326
  scroll_to_output=True
 
5
  import modules.version_info as version_info
6
  import modules.storage as storage
7
 
8
+ import urllib.request
9
+ from urllib.parse import urlparse, parse_qs, urlencode
10
 
11
  user_dir = constants.TMPDIR
12
  default_folder = "saved_models/3d_model_" + format(random.randint(1, 999999), "06d")
13
 
14
+ def _resolve_short_id_to_query_params(query_params: dict) -> dict:
15
+ """
16
+ Checks for a 'sid' (short ID) in query_params. If found, attempts to resolve
17
+ it to a full URL and updates query_params with the parameters ('3d', 'hm', 'image')
18
+ from the resolved URL.
19
+ """
20
+ short_id = query_params.get("sid")
21
+ if short_id:
22
+ status, full_url_from_shortener = storage.gen_full_url(
23
+ short_url=short_id,
24
+ repo_id=constants.HF_REPO_ID,
25
+ json_file=constants.SHORTENER_JSON_FILE
26
+ )
27
+ if status == "success_retrieved_full" and full_url_from_shortener:
28
+ print(f"Retrieved full URL from short ID '{short_id}': {full_url_from_shortener}")
29
+ try:
30
+ parsed_full_url = urlparse(full_url_from_shortener)
31
+ retrieved_params = parse_qs(parsed_full_url.query)
32
+
33
+ # Update query_params with those from the full_url_from_shortener
34
+ # The parse_qs function returns lists for values, so get the first element.
35
+ # If a param is not in the resolved URL, it will be set to None.
36
+ query_params["3d"] = retrieved_params.get("3d", [None])[0]
37
+ query_params["hm"] = retrieved_params.get("hm", [None])[0]
38
+ query_params["image"] = retrieved_params.get("image", [None])[0]
39
+ except Exception as e:
40
+ print(f"Error parsing full URL from shortener: {e}")
41
+ # Proceed with original query_params if parsing fails (i.e., don't overwrite them with Nones here)
42
+ else:
43
+ print(f"Failed to retrieve full URL for short ID '{short_id}': {status}")
44
+ # If sid resolution fails, original query_params (including potentially 3d, hm, image if passed alongside sid) remain.
45
+ return query_params
46
+
47
  def getVersions():
48
  #return html_versions
49
  return version_info.versions_html()
 
65
 
66
  # Parse URL to get components
67
  try:
 
 
68
 
69
  # Create filename from URL.
70
  parsed_url = urlparse(url)
 
131
  print(f"Error downloading file {url}: {e}")
132
  return url # Return original URL if download fails
133
 
 
134
  def load_data(request: gr.Request, model_3d, image_slider):
135
  """
136
  Load data from query parameters, download files if needed,
 
153
  # Parse query parameters.
154
  query_params = dict(request.query_params) if request is not None else {}
155
 
156
+ # Resolve short ID if present
157
+ query_params = _resolve_short_id_to_query_params(query_params)
158
+
159
+ # Extract URLs from query parameters (which may have been updated)
160
  model_url = query_params.get("3d", None)
161
  hm_url = query_params.get("hm", None)
162
  img_url = query_params.get("image", None)
163
 
164
+ # Check if any relevant parameters are set to decide if we should clear query_params for permalink generation
165
+ # This logic ensures that if only 'sid' was passed and it resolved, query_params will now contain 3d, hm, image.
166
+ # If 'sid' was passed but didn't resolve, or no params were passed, then model_url, hm_url, img_url will be None.
167
+ has_loadable_params = bool(model_url or hm_url or img_url)
168
+
169
  # Process the model URL if provided.
170
  if model_url:
171
  model_url = process_url(model_url, default_ext=".glb")
 
179
  if hm_url:
180
  local_hm = process_url(hm_url, default_ext=".png")
181
  if local_hm:
182
+ if not slider_images or local_hm != slider_images[0]:
183
+ if len(slider_images) == 1 and img_url:
184
+ slider_images.append(local_hm)
185
+ else:
186
+ slider_images = [local_hm] + slider_images
187
+ slider_images = slider_images[:2]
188
 
189
 
190
+ # Set default values if no URLs provided or processed:
191
+ default_model_val = getattr(model_3d, "value", model_3d)
192
+ default_images_val = getattr(image_slider, "value", image_slider)
193
 
194
  if not slider_images:
195
+ slider_images = default_images_val if default_images_val and default_images_val != (None, None) else constants.default_slider_images
196
 
197
  if not model_url:
198
+ model_url = default_model_val if default_model_val else constants.default_model_3d
199
 
200
+ # If any loadable query parameters were effectively present (either directly or via sid), generate a permalink.
201
  permalink = ""
202
+ if has_loadable_params:
203
  try:
204
+ permalink_model_url = query_params.get("3d", model_url)
205
+ permalink_hm_url = query_params.get("hm", hm_url if len(slider_images) > 1 and hm_url else (slider_images[1] if len(slider_images) > 1 else None) )
206
+ permalink_img_url = query_params.get("image", img_url if slider_images and img_url else (slider_images[0] if slider_images else None) )
207
+
208
+ permalink = storage.generate_permalink_from_urls(permalink_model_url, permalink_hm_url, permalink_img_url)
209
  except Exception as e:
210
  print(f"Error generating permalink: {e}")
211
 
 
256
 
257
  # For the image slider, we expect a list of exactly 2 images.
258
  # Start with current images (or use defaults if None).
259
+ if current_images is None or not isinstance(current_images, list) or len(current_images) == 0:
260
  new_images = [None, None]
261
  else:
262
+ new_images = current_images[:2]
263
+ if len(new_images) < 2:
264
+ new_images.append(None)
265
+
266
+ if len(extracted_images) == 1:
267
+ new_images[0] = extracted_images[0]
268
+ elif len(extracted_images) == 2:
269
+ new_images[0] = extracted_images[0]
270
+ new_images[1] = extracted_images[1]
271
+
272
  return updated_model, new_images
273
 
274
+ def get_open_graph_meta_tags(query_params):
275
+ """Generates Open Graph meta tags based on query parameters."""
276
+ og_title = constants.DEFAULT_OG_TITLE
277
+ og_description = constants.DEFAULT_OG_DESCRIPTION
278
+ og_image = constants.DEFAULT_OG_IMAGE_URL
279
+ og_type = constants.DEFAULT_OG_TYPE
280
+ og_url = constants.APP_BASE_URL
281
+
282
+ img_url_from_query = query_params.get("image")
283
+ if img_url_from_query:
284
+ og_image = img_url_from_query
285
+
286
+ model_url_from_query = query_params.get("3d")
287
+ if model_url_from_query:
288
+ try:
289
+ model_filename = os.path.basename(urlparse(model_url_from_query).path)
290
+ if model_filename:
291
+ og_title = f"3D Viewer: {model_filename}"
292
+ else:
293
+ og_title = f"Shared 3D Model"
294
+ except Exception:
295
+ og_title = "Shared 3D Model"
296
+ else:
297
+ og_title = "Shared Image"
298
+
299
+ if query_params:
300
+ filtered_query_params = {k: v for k, v in query_params.items() if v is not None}
301
+ if filtered_query_params:
302
+ og_url += "?" + urlencode(filtered_query_params)
303
+
304
+ meta_tags = f'''
305
+ <meta property="og:title" content="{og_title}" />
306
+ <meta property="og:description" content="{og_description}" />
307
+ <meta property="og:image" content="{og_image}" />
308
+ <meta property="og:url" content="{og_url}" />
309
+ <meta property="og:type" content="{og_type}" />
310
+ <meta name="twitter:card" content="summary_large_image">
311
+ <meta name="twitter:title" content="{og_title}">
312
+ <meta name="twitter:description" content="{og_description}">
313
+ <meta name="twitter:image" content="{og_image}">
314
+ '''
315
+ return meta_tags
316
+
317
+ placeholder_initial_query_params = {}
318
+ processed_placeholder_params = _resolve_short_id_to_query_params(placeholder_initial_query_params)
319
+ initial_og_tags = get_open_graph_meta_tags(processed_placeholder_params)
320
+
321
  gr.set_static_paths(paths=["images/", "models/", "assets/"])
322
+ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/beeuty',delete_cache=(21600,86400), fill_width=True, head=initial_og_tags) as viewer3d:
323
  gr.Markdown("# 3D Model Viewer")
324
 
325
  with gr.Row():
 
350
  )
351
 
352
  with gr.Row():
 
353
  folder_name_box = gr.Textbox(
354
  label="Folder Name",
355
  value=default_folder,
 
374
  with gr.Row():
375
  gr.HTML(value=getVersions(), visible=True, elem_id="versions")
376
 
 
377
  viewer3d.load(
378
  load_data,
379
  inputs=[model_3d, image_slider],
380
  outputs=[model_3d, image_slider, permalink],
381
  scroll_to_output=True
382
  ).then(
 
 
383
  lambda link: (gr.update(visible=True), gr.update(interactive=False))
384
  if link and len(link) > 0
385
  else (gr.update(visible=False), gr.update(interactive=True)),
 
387
  outputs=[permalink_row, permalink_button]
388
  )
389
 
 
390
  upload_btn.upload(
391
  process_upload,
392
  inputs=[upload_btn, model_3d, image_slider],
 
395
  api_name="process_upload",
396
  show_progress=True
397
  ).then(
 
398
  lambda m, i: gr.update(interactive=True),
399
  inputs=[model_3d, image_slider],
400
  outputs=[permalink_button]
401
  )
 
402
  permalink_button.click(
403
  lambda model, images, folder: storage.upload_files_to_repo(
404
+ files=[model] + list(images if images else []),
405
+ repo_id=constants.HF_REPO_ID,
406
  folder_name=folder,
407
  create_permalink=True,
408
  repo_type="dataset"
409
+ )[1],
410
  inputs=[model_3d, image_slider, folder_name_box],
411
  outputs=[permalink],
412
  scroll_to_output=True
modules/constants.py CHANGED
@@ -34,4 +34,15 @@ default_slider_images = [
34
  "images/slider/beeuty_545jlbh1_v12_alpha96_300dpi_depth.png"
35
  ]
36
 
37
- default_model_3d = "models/beeuty_545jlbh1_300dpi.glb"
 
 
 
 
 
 
 
 
 
 
 
 
34
  "images/slider/beeuty_545jlbh1_v12_alpha96_300dpi_depth.png"
35
  ]
36
 
37
+ default_model_3d = "models/beeuty_545jlbh1_300dpi.glb"
38
+
39
+ # Constants for URL shortener
40
+ HF_REPO_ID = "Surn/Storage" # Replace with your Hugging Face repository ID
41
+ SHORTENER_JSON_FILE = "shortener.json" # The name of your JSON file in the repo
42
+
43
+ # Open Graph Defaults
44
+ APP_BASE_URL = "https://surn-3d-viewer.hf.space" # Replace if your app URL is different
45
+ DEFAULT_OG_TITLE = "3D Viewer"
46
+ DEFAULT_OG_DESCRIPTION = "View and share 3D models and images interactively."
47
+ DEFAULT_OG_IMAGE_URL = APP_BASE_URL + "/gradio_api/file=" + default_slider_images[0]
48
+ DEFAULT_OG_TYPE = "website"
modules/storage.py CHANGED
@@ -3,7 +3,10 @@ import os
3
  import urllib.parse
4
  import tempfile
5
  import shutil
6
- from huggingface_hub import login, upload_folder
 
 
 
7
  from modules.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions
8
 
9
  def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"):
@@ -125,3 +128,169 @@ def upload_files_to_repo(files, repo_id, folder_name, create_permalink=False, re
125
 
126
  # Otherwise, return individual tuples for each file.
127
  return [(response, link) for link in individual_links]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import urllib.parse
4
  import tempfile
5
  import shutil
6
+ import json
7
+ import base64
8
+ from huggingface_hub import login, upload_folder, hf_hub_download, HfApi
9
+ from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
10
  from modules.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions
11
 
12
  def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"):
 
128
 
129
  # Otherwise, return individual tuples for each file.
130
  return [(response, link) for link in individual_links]
131
+
132
+ def _generate_short_id(length=8):
133
+ """Generates a random base64 URL-safe string."""
134
+ return base64.urlsafe_b64encode(os.urandom(length * 2))[:length].decode('utf-8')
135
+
136
+ def _get_json_from_repo(repo_id, json_file_name, repo_type="dataset"):
137
+ """Downloads and loads the JSON file from the repo. Returns empty list if not found or error."""
138
+ try:
139
+ login(token=HF_API_TOKEN)
140
+ json_path = hf_hub_download(
141
+ repo_id=repo_id,
142
+ filename=json_file_name,
143
+ repo_type=repo_type,
144
+ token=HF_API_TOKEN # Added token for consistency, though login might suffice
145
+ )
146
+ with open(json_path, 'r') as f:
147
+ data = json.load(f)
148
+ os.remove(json_path) # Clean up downloaded file
149
+ return data
150
+ except RepositoryNotFoundError:
151
+ print(f"Repository {repo_id} not found.")
152
+ return []
153
+ except EntryNotFoundError:
154
+ print(f"JSON file {json_file_name} not found in {repo_id}. Initializing with empty list.")
155
+ return []
156
+ except json.JSONDecodeError:
157
+ print(f"Error decoding JSON from {json_file_name}. Returning empty list.")
158
+ return []
159
+ except Exception as e:
160
+ print(f"An unexpected error occurred while fetching {json_file_name}: {e}")
161
+ return []
162
+
163
+ def _upload_json_to_repo(data, repo_id, json_file_name, repo_type="dataset"):
164
+ """Uploads the JSON data to the specified file in the repo."""
165
+ try:
166
+ login(token=HF_API_TOKEN)
167
+ api = HfApi()
168
+ with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".json") as tmp_file:
169
+ json.dump(data, tmp_file, indent=2)
170
+ tmp_file_path = tmp_file.name
171
+
172
+ api.upload_file(
173
+ path_or_fileobj=tmp_file_path,
174
+ path_in_repo=json_file_name,
175
+ repo_id=repo_id,
176
+ repo_type=repo_type,
177
+ commit_message=f"Update {json_file_name}"
178
+ )
179
+ os.remove(tmp_file_path) # Clean up temporary file
180
+ return True
181
+ except Exception as e:
182
+ print(f"Failed to upload {json_file_name} to {repo_id}: {e}")
183
+ if 'tmp_file_path' in locals() and os.path.exists(tmp_file_path):
184
+ os.remove(tmp_file_path) # Ensure cleanup on error too
185
+ return False
186
+
187
+ def _find_url_in_json(data, short_url=None, full_url=None):
188
+ """
189
+ Searches the JSON data.
190
+ If short_url is provided, returns the corresponding full_url or None.
191
+ If full_url is provided, returns the corresponding short_url or None.
192
+ """
193
+ if not data: # Handles cases where data might be None or empty
194
+ return None
195
+ if short_url:
196
+ for item in data:
197
+ if item.get("short_url") == short_url:
198
+ return item.get("full_url")
199
+ if full_url:
200
+ for item in data:
201
+ if item.get("full_url") == full_url:
202
+ return item.get("short_url")
203
+ return None
204
+
205
+ def _add_url_to_json(data, short_url, full_url):
206
+ """Adds a new short_url/full_url pair to the data. Returns updated data."""
207
+ if data is None: # Initialize if data is None
208
+ data = []
209
+ data.append({"short_url": short_url, "full_url": full_url})
210
+ return data
211
+
212
+ def gen_full_url(short_url=None, full_url=None, repo_id=None, repo_type="dataset", permalink_viewer_url="surn-3d-viewer.hf.space", json_file="shortener.json"):
213
+ """
214
+ Manages short URLs and their corresponding full URLs in a JSON file stored in a Hugging Face repository.
215
+
216
+ - If short_url is provided, attempts to retrieve and return the full_url.
217
+ - If full_url is provided, attempts to retrieve an existing short_url or creates a new one, stores it, and returns the short_url.
218
+ - If both are provided, checks for consistency or creates a new entry.
219
+ - If neither is provided, or repo_id is missing, returns an error status.
220
+
221
+ Returns:
222
+ tuple: (status_message, result_url)
223
+ status_message can be "success", "created", "exists", "error", "not_found".
224
+ result_url is the relevant URL (short or full) or None if an error occurs or not found.
225
+ """
226
+ if not repo_id:
227
+ return "error_repo_id_missing", None
228
+ if not short_url and not full_url:
229
+ return "error_no_input", None
230
+
231
+ login(token=HF_API_TOKEN) # Ensure login at the beginning
232
+ url_data = _get_json_from_repo(repo_id, json_file, repo_type)
233
+
234
+ # Case 1: Only short_url provided (lookup full_url)
235
+ if short_url and not full_url:
236
+ found_full_url = _find_url_in_json(url_data, short_url=short_url)
237
+ if found_full_url:
238
+ return "success_retrieved_full", found_full_url
239
+ else:
240
+ return "not_found_short", None
241
+
242
+ # Case 2: Only full_url provided (lookup or create short_url)
243
+ if full_url and not short_url:
244
+ existing_short_url = _find_url_in_json(url_data, full_url=full_url)
245
+ if existing_short_url:
246
+ return "success_retrieved_short", existing_short_url
247
+ else:
248
+ # Create new short_url
249
+ new_short_id = _generate_short_id()
250
+ # Construct the short URL using the permalink_viewer_url and the new_short_id as a query parameter or path
251
+ # For this example, let's assume short URLs are like: https://permalink_viewer_url/?id=new_short_id
252
+ # This part might need adjustment based on how you want to structure your short URLs.
253
+ # A common pattern is permalink_viewer_url/new_short_id if the viewer can handle path-based routing.
254
+ # Or, if the viewer expects a query param like `?short=new_short_id`
255
+ # For now, let's assume the short_url itself is just the ID, and the full viewer URL is prepended elsewhere if needed.
256
+ # Or, more directly, the `short_url` parameter to this function *is* the ID.
257
+ # The request implies `short_url` is the *key* in the JSON.
258
+
259
+ # Let's refine: the `short_url` stored in JSON is the ID. The "shortened URL" returned to user might be different.
260
+ # The function is `gen_full_url`, implying it can also *generate* a short URL if one doesn't exist for a full_url.
261
+
262
+ url_data = _add_url_to_json(url_data, new_short_id, full_url)
263
+ if _upload_json_to_repo(url_data, repo_id, json_file, repo_type):
264
+ # The value returned as "shortened_url" should be the ID itself, or a URL constructed with it.
265
+ # Let's return the ID for now, as the prompt asks for "shortened_url" as output.
266
+ return "created_short", new_short_id
267
+ else:
268
+ return "error_upload", None
269
+
270
+ # Case 3: Both short_url and full_url provided
271
+ if short_url and full_url:
272
+ found_full_for_short = _find_url_in_json(url_data, short_url=short_url)
273
+ found_short_for_full = _find_url_in_json(url_data, full_url=full_url)
274
+
275
+ if found_full_for_short and found_full_for_short == full_url: # Both exist and match
276
+ return "exists_match", short_url
277
+ elif found_short_for_full and found_short_for_full == short_url: # Both exist and match (redundant check, but safe)
278
+ return "exists_match", short_url
279
+ elif found_full_for_short and found_full_for_short != full_url: # short_url exists but maps to a different full_url
280
+ return "error_conflict_short_exists", short_url # Or perhaps update, depending on desired behavior
281
+ elif found_short_for_full and found_short_for_full != short_url: # full_url exists but maps to a different short_url
282
+ # This implies the user provided a short_url that is *not* the one already associated with this full_url.
283
+ # We should probably return the *existing* short_url for that full_url.
284
+ return "exists_full_maps_to_different_short", found_short_for_full
285
+ else: # Neither exists, or one exists but not the pair. Create new entry.
286
+ # Check if the provided short_url is already in use by another full_url
287
+ if _find_url_in_json(url_data, short_url=short_url) is not None:
288
+ return "error_conflict_short_id_taken", short_url
289
+
290
+ url_data = _add_url_to_json(url_data, short_url, full_url)
291
+ if _upload_json_to_repo(url_data, repo_id, json_file, repo_type):
292
+ return "created_specific_pair", short_url
293
+ else:
294
+ return "error_upload", None
295
+
296
+ return "error_unhandled_case", None # Should not be reached