Spaces:
Sleeping
Sleeping
def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column
Browse files- app.py +148 -23
- sheet_service.py +128 -2
app.py
CHANGED
|
@@ -2157,10 +2157,135 @@ def summary_add_markdown_version(video_id):
|
|
| 2157 |
def get_sheet_data(sheet_url, range_name):
|
| 2158 |
data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
|
| 2159 |
flattened_data = SHEET_SERVICE.flatten_column_data(data)
|
| 2160 |
-
|
|
|
|
| 2161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2162 |
|
| 2163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2164 |
# 輸入影片 id,以 , 逗號分隔 或是 \n 換行
|
| 2165 |
video_id_list = video_ids.replace('\n', ',').split(',')
|
| 2166 |
video_id_list = [vid.strip() for vid in video_id_list if vid.strip()]
|
|
@@ -2170,18 +2295,7 @@ def refresh_video_LLM_all_content(video_ids):
|
|
| 2170 |
|
| 2171 |
for video_id in video_id_list:
|
| 2172 |
try:
|
| 2173 |
-
|
| 2174 |
-
print(f"video_id: {video_id}")
|
| 2175 |
-
# 刪除 GCS 中所有以 video_id 開頭的檔案
|
| 2176 |
-
print(f"===delete_blobs_by_folder_name: {video_id}===")
|
| 2177 |
-
bucket_name = 'video_ai_assistant'
|
| 2178 |
-
GCS_SERVICE.delete_blobs_by_folder_name(bucket_name, video_id)
|
| 2179 |
-
print(f"所有以 {video_id} 開頭的檔案已刪除")
|
| 2180 |
-
|
| 2181 |
-
# process_youtube_link
|
| 2182 |
-
video_link = f"https://www.youtube.com/watch?v={video_id}"
|
| 2183 |
-
process_youtube_link(PASSWORD, video_link)
|
| 2184 |
-
|
| 2185 |
success_video_ids.append(video_id)
|
| 2186 |
except Exception as e:
|
| 2187 |
print(f"===refresh_all_LLM_content error===")
|
|
@@ -3663,16 +3777,11 @@ def create_app():
|
|
| 3663 |
refresh_btn = gr.Button("refresh", variant="primary")
|
| 3664 |
with gr.Tab("by sheets"):
|
| 3665 |
sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
|
|
|
|
|
|
|
| 3666 |
sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
|
| 3667 |
sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
|
| 3668 |
-
sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
|
| 3669 |
-
|
| 3670 |
-
sheet_get_value_btn.click(
|
| 3671 |
-
get_sheet_data,
|
| 3672 |
-
inputs=[sheet_url, gr.Textbox(value="D:D", visible=True)], # 將範圍修改為 D 欄
|
| 3673 |
-
outputs=[sheet_get_value_result]
|
| 3674 |
-
)
|
| 3675 |
-
|
| 3676 |
with gr.Row():
|
| 3677 |
refresh_result = gr.JSON()
|
| 3678 |
|
|
@@ -3681,10 +3790,26 @@ def create_app():
|
|
| 3681 |
inputs=[],
|
| 3682 |
outputs=[refresh_btn]
|
| 3683 |
).then(
|
| 3684 |
-
|
| 3685 |
inputs=[refresh_video_ids],
|
| 3686 |
outputs=[refresh_result]
|
| 3687 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3688 |
|
| 3689 |
|
| 3690 |
# OPEN AI CHATBOT SELECT
|
|
|
|
| 2157 |
def get_sheet_data(sheet_url, range_name):
|
| 2158 |
data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
|
| 2159 |
flattened_data = SHEET_SERVICE.flatten_column_data(data)
|
| 2160 |
+
flattened_data_string = ', '.join(flattened_data)
|
| 2161 |
+
return flattened_data_string
|
| 2162 |
|
| 2163 |
+
def update_sheet_data(sheet_url, qa_result, video_id):
|
| 2164 |
+
# 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
|
| 2165 |
+
sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
|
| 2166 |
+
print(f"sheet_data: {sheet_data}")
|
| 2167 |
|
| 2168 |
+
if not sheet_data or len(sheet_data) < 1:
|
| 2169 |
+
print("錯誤:工作表資料為空或缺少標頭列。")
|
| 2170 |
+
return
|
| 2171 |
+
|
| 2172 |
+
header_row = sheet_data[0]
|
| 2173 |
+
data_rows = sheet_data[1:] # 資料列(跳過標頭)
|
| 2174 |
+
|
| 2175 |
+
# 直接指定要尋找的欄位名稱
|
| 2176 |
+
target_column_name = '均一平台 YT Readable ID'
|
| 2177 |
+
target_qa_column_name = "QA"
|
| 2178 |
+
|
| 2179 |
+
try:
|
| 2180 |
+
# 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
|
| 2181 |
+
video_id_col_index = header_row.index(target_column_name)
|
| 2182 |
+
except ValueError:
|
| 2183 |
+
# 如果找不到指定的欄位名稱,印出錯誤並返回
|
| 2184 |
+
print(f"錯誤:在標頭列 {header_row} 中找不到指定的欄位名稱 '{target_column_name}'")
|
| 2185 |
+
return
|
| 2186 |
+
|
| 2187 |
+
# 2. 遍歷資料列,尋找 video_id 匹配的列索引 (row index)
|
| 2188 |
+
target_row_index = -1 # 初始化為 -1,表示未找到
|
| 2189 |
+
for i, row in enumerate(data_rows):
|
| 2190 |
+
# 確保該列有足夠的欄位,並且該欄位的值等於 video_id
|
| 2191 |
+
if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
|
| 2192 |
+
target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1
|
| 2193 |
+
break # 找到後即可跳出迴圈
|
| 2194 |
+
|
| 2195 |
+
if target_row_index != -1:
|
| 2196 |
+
print(f"找到 video_id '{video_id}' 於欄位 '{target_column_name}' 的第 {target_row_index + 1} 列 (工作表列號)") # +1 是為了顯示給人類看的行號
|
| 2197 |
+
|
| 2198 |
+
# --- 以下是更新邏輯 (需要取消註解並確保 qa_column 也正確) ---
|
| 2199 |
+
try:
|
| 2200 |
+
# 找到 QA 欄位的索引 (假設 qa_column 參數傳入的是正確的標題文字, e.g., 'QA')
|
| 2201 |
+
qa_col_index = header_row.index(target_qa_column_name)
|
| 2202 |
+
|
| 2203 |
+
# 更新 sheet 中的 qa_column 和 qa_result
|
| 2204 |
+
# 確保目標列有足夠的欄位可以更新
|
| 2205 |
+
if len(sheet_data[target_row_index]) > qa_col_index:
|
| 2206 |
+
# 注意:這裡直接修改 sheet_data 列表可能不會直接更新 Google Sheet
|
| 2207 |
+
# 你需要使用 SHEET_SERVICE 的更新方法
|
| 2208 |
+
# sheet_data[target_row_index][qa_col_index] = qa_result
|
| 2209 |
+
print(f"準備更新 第 {target_row_index + 1} 列, 欄位 '{target_qa_column_name}' (索引 {qa_col_index}) 為 '{qa_result}'")
|
| 2210 |
+
# --- 實際更新 Google Sheet 的程式碼
|
| 2211 |
+
SHEET_SERVICE.update_sheet_cell(sheet_url, target_row_index, qa_col_index, qa_result)
|
| 2212 |
+
|
| 2213 |
+
else:
|
| 2214 |
+
print(f"錯誤:第 {target_row_index + 1} 列沒有足夠的欄位來更新 QA (索引 {qa_col_index})。")
|
| 2215 |
+
|
| 2216 |
+
except ValueError:
|
| 2217 |
+
print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{qa_column}'")
|
| 2218 |
+
# --- 更新邏輯結束 ---
|
| 2219 |
+
|
| 2220 |
+
else:
|
| 2221 |
+
# 如果找不到 video_id,印出錯誤訊息
|
| 2222 |
+
print(f"錯誤:在欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
|
| 2223 |
+
|
| 2224 |
+
def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids): # 移除 sheet_video_column
|
| 2225 |
+
video_ids = video_ids.replace('\n', ',').split(',')
|
| 2226 |
+
video_ids = [vid.strip() for vid in video_ids if vid.strip()]
|
| 2227 |
+
|
| 2228 |
+
success_video_ids = []
|
| 2229 |
+
failed_video_ids = []
|
| 2230 |
+
|
| 2231 |
+
sheet_qa_success_tag = "OO"
|
| 2232 |
+
sheet_qa_failed_tag = "XX"
|
| 2233 |
+
|
| 2234 |
+
for video_id in video_ids:
|
| 2235 |
+
try:
|
| 2236 |
+
# 確認 GCS 中是否存在 ID_transcript.json in GCS
|
| 2237 |
+
# 如果存在就 pass
|
| 2238 |
+
# 如果不存在就 refresh_video_LLM_all_content_by_id
|
| 2239 |
+
bucket_name = 'video_ai_assistant'
|
| 2240 |
+
file_name = f'{video_id}_transcript.json'
|
| 2241 |
+
blob_name = f"{video_id}/{file_name}"
|
| 2242 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
| 2243 |
+
if not is_file_exists:
|
| 2244 |
+
print(f"{video_id} 不存在逐字稿")
|
| 2245 |
+
refresh_video_LLM_all_content_by_id(video_id)
|
| 2246 |
+
else:
|
| 2247 |
+
print(f"{video_id} 存在逐字稿")
|
| 2248 |
+
|
| 2249 |
+
qa_result = sheet_qa_success_tag
|
| 2250 |
+
# 更新呼叫,不再傳遞 sheet_video_column
|
| 2251 |
+
update_sheet_data(sheet_url, qa_result, video_id)
|
| 2252 |
+
success_video_ids.append(video_id) # 假設 update_sheet_data 成功就加入 success
|
| 2253 |
+
except Exception as e:
|
| 2254 |
+
print(f"===refresh_video_LLM_all_content_by_sheet error===")
|
| 2255 |
+
print(f"video_id: {video_id}")
|
| 2256 |
+
print(f"error: {str(e)}")
|
| 2257 |
+
print(f"===refresh_video_LLM_all_content_by_sheet error===")
|
| 2258 |
+
failed_video_ids.append(video_id)
|
| 2259 |
+
qa_result = sheet_qa_failed_tag
|
| 2260 |
+
try:
|
| 2261 |
+
# 即使前面出錯,還是嘗試更新 QA 狀態為 XX
|
| 2262 |
+
update_sheet_data(sheet_url, qa_result, video_id)
|
| 2263 |
+
except Exception as update_e:
|
| 2264 |
+
print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
|
| 2265 |
+
print(f"video_id: {video_id}")
|
| 2266 |
+
print(f"error: {str(update_e)}")
|
| 2267 |
+
print(f"===更新 QA 狀態為 XX 時也發生錯誤===")
|
| 2268 |
+
|
| 2269 |
+
|
| 2270 |
+
result = {
|
| 2271 |
+
"success_video_ids": success_video_ids,
|
| 2272 |
+
"failed_video_ids": failed_video_ids
|
| 2273 |
+
}
|
| 2274 |
+
return result
|
| 2275 |
+
|
| 2276 |
+
def refresh_video_LLM_all_content_by_id(video_id):
|
| 2277 |
+
print(f"===refresh_all_LLM_content===")
|
| 2278 |
+
print(f"video_id: {video_id}")
|
| 2279 |
+
print(f"===delete_blobs_by_folder_name: {video_id}===")
|
| 2280 |
+
bucket_name = 'video_ai_assistant'
|
| 2281 |
+
GCS_SERVICE.delete_blobs_by_folder_name(bucket_name, video_id)
|
| 2282 |
+
print(f"所有以 {video_id} 開頭的檔案已刪除")
|
| 2283 |
+
|
| 2284 |
+
# process_youtube_link
|
| 2285 |
+
video_link = f"https://www.youtube.com/watch?v={video_id}"
|
| 2286 |
+
process_youtube_link(PASSWORD, video_link)
|
| 2287 |
+
|
| 2288 |
+
def refresh_video_LLM_all_content_by_ids(video_ids):
|
| 2289 |
# 輸入影片 id,以 , 逗號分隔 或是 \n 換行
|
| 2290 |
video_id_list = video_ids.replace('\n', ',').split(',')
|
| 2291 |
video_id_list = [vid.strip() for vid in video_id_list if vid.strip()]
|
|
|
|
| 2295 |
|
| 2296 |
for video_id in video_id_list:
|
| 2297 |
try:
|
| 2298 |
+
refresh_video_LLM_all_content_by_id(video_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2299 |
success_video_ids.append(video_id)
|
| 2300 |
except Exception as e:
|
| 2301 |
print(f"===refresh_all_LLM_content error===")
|
|
|
|
| 3777 |
refresh_btn = gr.Button("refresh", variant="primary")
|
| 3778 |
with gr.Tab("by sheets"):
|
| 3779 |
sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
|
| 3780 |
+
sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="D:D")
|
| 3781 |
+
sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="F:F")
|
| 3782 |
sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
|
| 3783 |
sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
|
| 3784 |
+
sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3785 |
with gr.Row():
|
| 3786 |
refresh_result = gr.JSON()
|
| 3787 |
|
|
|
|
| 3790 |
inputs=[],
|
| 3791 |
outputs=[refresh_btn]
|
| 3792 |
).then(
|
| 3793 |
+
refresh_video_LLM_all_content_by_ids,
|
| 3794 |
inputs=[refresh_video_ids],
|
| 3795 |
outputs=[refresh_result]
|
| 3796 |
)
|
| 3797 |
+
|
| 3798 |
+
sheet_get_value_btn.click(
|
| 3799 |
+
get_sheet_data,
|
| 3800 |
+
inputs=[sheet_url, sheet_video_column],
|
| 3801 |
+
outputs=[sheet_get_value_result]
|
| 3802 |
+
)
|
| 3803 |
+
|
| 3804 |
+
sheet_refresh_btn.click(
|
| 3805 |
+
lambda: gr.update(interactive=False),
|
| 3806 |
+
inputs=[],
|
| 3807 |
+
outputs=[sheet_refresh_btn]
|
| 3808 |
+
).then(
|
| 3809 |
+
refresh_video_LLM_all_content_by_sheet,
|
| 3810 |
+
inputs=[sheet_url, sheet_QA_column, sheet_get_value_result],
|
| 3811 |
+
outputs=[refresh_result]
|
| 3812 |
+
)
|
| 3813 |
|
| 3814 |
|
| 3815 |
# OPEN AI CHATBOT SELECT
|
sheet_service.py
CHANGED
|
@@ -4,6 +4,7 @@ from google.oauth2 import service_account
|
|
| 4 |
import json
|
| 5 |
from urllib.parse import urlparse, parse_qs
|
| 6 |
import logging # 建議使用 logging 而非 print
|
|
|
|
| 7 |
|
| 8 |
# 設定基本的 logging
|
| 9 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
@@ -12,7 +13,7 @@ class SheetService:
|
|
| 12 |
"""
|
| 13 |
一個用於與 Google Sheets API 互動的服務類別。
|
| 14 |
"""
|
| 15 |
-
SCOPES = ['https://www.googleapis.com/auth/spreadsheets
|
| 16 |
|
| 17 |
def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
|
| 18 |
"""
|
|
@@ -266,4 +267,129 @@ class SheetService:
|
|
| 266 |
# 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
|
| 267 |
# 並將其轉換為字串 str() 以確保類型一致性
|
| 268 |
flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
|
| 269 |
-
return flattened
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import json
|
| 5 |
from urllib.parse import urlparse, parse_qs
|
| 6 |
import logging # 建議使用 logging 而非 print
|
| 7 |
+
import string # 導入 string 模組用於轉換欄位索引
|
| 8 |
|
| 9 |
# 設定基本的 logging
|
| 10 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
| 13 |
"""
|
| 14 |
一個用於與 Google Sheets API 互動的服務類別。
|
| 15 |
"""
|
| 16 |
+
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
|
| 17 |
|
| 18 |
def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
|
| 19 |
"""
|
|
|
|
| 267 |
# 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
|
| 268 |
# 並將其轉換為字串 str() 以確保類型一致性
|
| 269 |
flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
|
| 270 |
+
return flattened
|
| 271 |
+
|
| 272 |
+
def update_sheet_data_by_url(self, sheet_url, data):
|
| 273 |
+
spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
|
| 274 |
+
self.sheet.values().update(
|
| 275 |
+
spreadsheetId=spreadsheet_id,
|
| 276 |
+
range='A1:Z1000',
|
| 277 |
+
valueInputOption='USER_ENTERED',
|
| 278 |
+
body={'values': data}
|
| 279 |
+
).execute()
|
| 280 |
+
|
| 281 |
+
return True
|
| 282 |
+
|
| 283 |
+
@staticmethod
|
| 284 |
+
def _col_index_to_letter(col_index: int) -> str:
|
| 285 |
+
"""將 0-based 的欄位索引轉換為 A1 表示法的字母。"""
|
| 286 |
+
if col_index < 0:
|
| 287 |
+
raise ValueError("Column index must be non-negative")
|
| 288 |
+
letters = ''
|
| 289 |
+
while col_index >= 0:
|
| 290 |
+
col_index, remainder = divmod(col_index, 26)
|
| 291 |
+
letters = string.ascii_uppercase[remainder] + letters
|
| 292 |
+
col_index -= 1 # 因為 divmod 是 0-based,但 A=1, Z=26, AA=27 的轉換需要調整
|
| 293 |
+
if col_index < -1: # 修正邊界條件
|
| 294 |
+
break
|
| 295 |
+
# 修正:上面的邏輯有點複雜且可能有誤,改用更簡單的方式
|
| 296 |
+
letters = ''
|
| 297 |
+
dividend = col_index + 1 # 轉為 1-based
|
| 298 |
+
while dividend > 0:
|
| 299 |
+
module = (dividend - 1) % 26
|
| 300 |
+
letters = string.ascii_uppercase[module] + letters
|
| 301 |
+
dividend = (dividend - module) // 26
|
| 302 |
+
return letters if letters else "A" # 確保至少返回 "A"
|
| 303 |
+
|
| 304 |
+
@staticmethod
|
| 305 |
+
def _col_index_to_letter_simple(col_index: int) -> str:
|
| 306 |
+
"""將 0-based 的欄位索引轉換為 A1 表示法的字母 (簡化版,適用於 A-ZZ)。"""
|
| 307 |
+
if col_index < 0:
|
| 308 |
+
raise ValueError("Column index must be non-negative")
|
| 309 |
+
letters = ""
|
| 310 |
+
while col_index >= 0:
|
| 311 |
+
letters = string.ascii_uppercase[col_index % 26] + letters
|
| 312 |
+
col_index = col_index // 26 - 1
|
| 313 |
+
return letters
|
| 314 |
+
|
| 315 |
+
# SHEET_SERVICE.update_sheet_cell(sheet_url, target_row_index, qa_col_index, qa_result)
|
| 316 |
+
def update_sheet_cell(self, sheet_url: str, target_row_index_in_data: int, qa_col_index: int, qa_result: str) -> bool:
|
| 317 |
+
"""
|
| 318 |
+
更新指定 URL 的 Google Sheet 中的單一儲存格。
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
sheet_url (str): Google 試算表的完整 URL。
|
| 322 |
+
target_row_index_in_data (int): 目標列在 get_sheet_data_by_url 返回的列表中的索引
|
| 323 |
+
(從 1 開始算,因為 0 是標頭)。
|
| 324 |
+
qa_col_index (int): 目標欄位的索引 (從 0 開始算)。
|
| 325 |
+
qa_result (str): 要寫入儲存格的值。
|
| 326 |
+
|
| 327 |
+
Returns:
|
| 328 |
+
bool: 更新是否成功。
|
| 329 |
+
"""
|
| 330 |
+
if not self.sheet:
|
| 331 |
+
logging.error("Sheet API 服務未成功初始化。")
|
| 332 |
+
return False
|
| 333 |
+
|
| 334 |
+
spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
|
| 335 |
+
if not spreadsheet_id:
|
| 336 |
+
logging.error(f"無法從 URL 獲取 Spreadsheet ID: {sheet_url}")
|
| 337 |
+
return False
|
| 338 |
+
|
| 339 |
+
gid = self.get_sheet_gid_by_url(sheet_url)
|
| 340 |
+
sheet_name = self.get_sheet_name_by_gid(spreadsheet_id, gid)
|
| 341 |
+
if not sheet_name:
|
| 342 |
+
logging.error(f"無法根據 URL ({sheet_url}) 確定要更新的工作表名稱。")
|
| 343 |
+
return False
|
| 344 |
+
|
| 345 |
+
# 將 0-based 的欄位索引轉換為字母
|
| 346 |
+
try:
|
| 347 |
+
col_letter = self._col_index_to_letter_simple(qa_col_index)
|
| 348 |
+
except ValueError as e:
|
| 349 |
+
logging.error(f"無效的欄位索引 {qa_col_index}: {e}")
|
| 350 |
+
return False
|
| 351 |
+
|
| 352 |
+
# 計算實際的工作表列號
|
| 353 |
+
# target_row_index_in_data 是 sheet_data 中的索引 (1-based)
|
| 354 |
+
# 實際列號是 target_row_index_in_data + 1 (因為工作表通常從第 1 列開始)
|
| 355 |
+
actual_sheet_row = target_row_index_in_data + 1
|
| 356 |
+
|
| 357 |
+
# 構建 A1 表示法的範圍,例如 '測試'!F3
|
| 358 |
+
# 需要正確處理工作表名稱中的特殊字符(例如空格)
|
| 359 |
+
if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name or "'" in sheet_name:
|
| 360 |
+
# 修正:使用三引號定義 f-string 以避免引號衝突
|
| 361 |
+
safe_sheet_name = f"""'{sheet_name.replace("'", "''")}'""" # 單引號用兩個單引號轉義
|
| 362 |
+
else:
|
| 363 |
+
safe_sheet_name = sheet_name
|
| 364 |
+
range_name = f"{safe_sheet_name}!{col_letter}{actual_sheet_row}"
|
| 365 |
+
|
| 366 |
+
logging.info(f"準備更新 Spreadsheet ID: {spreadsheet_id}, Range: {range_name}, Value: {qa_result}")
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
body = {
|
| 370 |
+
'values': [[qa_result]] # 更新單一儲存格的值需要是 list of lists
|
| 371 |
+
}
|
| 372 |
+
result = self.sheet.values().update(
|
| 373 |
+
spreadsheetId=spreadsheet_id,
|
| 374 |
+
range=range_name,
|
| 375 |
+
valueInputOption='USER_ENTERED', # 或者 'RAW' 如果你不需要 Google Sheets 解釋輸入
|
| 376 |
+
body=body
|
| 377 |
+
).execute()
|
| 378 |
+
logging.info(f"成功更新儲存格 {range_name}。 更新了 {result.get('updatedCells')} 個儲存格。")
|
| 379 |
+
return True
|
| 380 |
+
except googleapiclient.errors.HttpError as error:
|
| 381 |
+
# 記錄更詳細的錯誤
|
| 382 |
+
error_details = error.resp.get('content', '{}')
|
| 383 |
+
try:
|
| 384 |
+
error_json = json.loads(error_details)
|
| 385 |
+
error_message = error_json.get('error', {}).get('message', str(error))
|
| 386 |
+
except json.JSONDecodeError:
|
| 387 |
+
error_message = str(error)
|
| 388 |
+
logging.error(f"更新儲存格時發生 API 錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {error_message}")
|
| 389 |
+
# 檢查是否是權限錯誤
|
| 390 |
+
if error.resp.status == 403:
|
| 391 |
+
logging.error("錯誤 403:權限不足。請檢查服務帳戶是否有目標工作表的編輯權限,以及 API 金鑰是否啟用了正確的範圍 (scopes)。")
|
| 392 |
+
return False
|
| 393 |
+
except Exception as e:
|
| 394 |
+
logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
|
| 395 |
+
return False
|