turkiyem 1.4.0 → 1.6.0

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.
package/README.md CHANGED
@@ -1,17 +1,59 @@
1
1
  # turkiyem
2
2
 
3
- Turkiye Toplu Tasima ve Deprem CLI araci.
4
-
5
- AFAD deprem verileri, EGO (Ankara) otobus saatleri ve IETT (Istanbul) hat/saat bilgilerini terminalden sorgulayabilirsiniz.
6
- Open-Meteo ile API key gerektirmeden guncel hava, saatlik tahmin ve hava kalitesi sorgulayabilirsiniz.
3
+ Türkiye için modern, terminal tabanlı bir **toplu taşıma + deprem + hava durumu** CLI aracı.
4
+
5
+ `turkiyem`; AFAD deprem verileri, EGO (Ankara) hat saatleri, IETT (İstanbul) hat bilgileri / planlanan sefer saatleri / canlı konum akışları ve Open-Meteo hava verilerini tek komutta toplar.
6
+
7
+ ## İçindekiler
8
+
9
+ - [Özellikler](#özellikler)
10
+ - [Kurulum](#kurulum)
11
+ - [Hızlı Başlangıç](#hızlı-başlangıç)
12
+ - [Komut Referansı](#komut-referansı)
13
+ - [Veri Kaynakları](#veri-kaynakları)
14
+ - [Mimari ve Proje Yapısı](#mimari-ve-proje-yapısı)
15
+ - [Yapılandırma ve Cache](#yapılandırma-ve-cache)
16
+ - [Hata Yönetimi](#hata-yönetimi)
17
+ - [Geliştirme](#geliştirme)
18
+ - [Yayınlama (npm)](#yayınlama-npm)
19
+ - [Sık Karşılaşılan Sorunlar](#sık-karşılaşılan-sorunlar)
20
+ - [Lisans](#lisans)
21
+
22
+ ## Özellikler
23
+
24
+ - **Deprem (AFAD):**
25
+ - Son 24 saat / son 7 gün deprem listesi
26
+ - Büyüklüğe göre filtreleme
27
+ - `>= 4.0` depremler için renkli vurgu
28
+ - **Toplu taşıma (Ankara / EGO):**
29
+ - Hat bilgileri
30
+ - Gün tipine göre sefer saatleri (Hafta içi / Cumartesi / Pazar)
31
+ - **Toplu taşıma (İstanbul / IETT):**
32
+ - GTFS tabanlı hat özeti
33
+ - SOAP tabanlı planlanan sefer saatleri
34
+ - SOAP başarısız olduğunda GTFS özete otomatik fallback
35
+ - `hat canli` ile canlı araç konumu (servis erişimine bağlı)
36
+ - **Hava durumu (Open-Meteo):**
37
+ - API key gerektirmeden güncel hava
38
+ - Saatlik tahmin (1-7 gün)
39
+ - Hava kalitesi (PM10, PM2.5, CO, NO2)
40
+ - **Ekonomi:**
41
+ - TCMB güncel döviz kurları listesi
42
+ - **CLI UX:**
43
+ - Komut bazlı spinner
44
+ - Tablo tabanlı okunabilir terminal çıktısı
45
+ - Global şehir ayarı (`~/.turkiyem/config.json`)
46
+ - Bellek içi cache ile performans optimizasyonu
7
47
 
8
48
  ## Kurulum
9
49
 
50
+ ### Global (önerilen)
51
+
10
52
  ```bash
11
53
  npm install -g turkiyem
12
54
  ```
13
55
 
14
- Veya yerel olarak:
56
+ ### Yerel geliştirme
15
57
 
16
58
  ```bash
17
59
  git clone <repo-url>
@@ -20,144 +62,197 @@ npm install
20
62
  npm link
21
63
  ```
22
64
 
23
- ## Gereksinimler
65
+ ### Gereksinim
66
+
67
+ - Node.js `20+`
24
68
 
25
- - Node.js 20+
69
+ ## Hızlı Başlangıç
70
+
71
+ ```bash
72
+ turkiyem
73
+ turkiyem sehir istanbul
74
+ turkiyem hat 34AS
75
+ turkiyem deprem son24
76
+ turkiyem hava guncel
77
+ turkiyem doviz
78
+ ```
26
79
 
27
- ## Kullanim
80
+ ## Komut Referansı
28
81
 
29
- ### Banner ve Yardim
82
+ ### Genel
30
83
 
31
84
  ```bash
32
85
  turkiyem
33
86
  turkiyem help
87
+ turkiyem --version
88
+ turkiyem temizle
34
89
  ```
35
90
 
36
- ### Sehir Secimi
37
-
38
- Hat sorgulama icin once sehir secmelisiniz:
91
+ ### Şehir seçimi
39
92
 
40
93
  ```bash
41
94
  turkiyem sehir ankara
42
95
  turkiyem sehir istanbul
43
96
  ```
44
97
 
45
- ### Hat Sorgulama
98
+ > `hat` komutları seçili şehre göre çalışır.
46
99
 
47
- Secili sehre gore hat bilgilerini sorgular:
100
+ ### Hat sorgulama
48
101
 
49
102
  ```bash
50
103
  # Ankara (EGO)
51
104
  turkiyem sehir ankara
52
105
  turkiyem hat 340
53
106
 
54
- # Istanbul (IETT)
107
+ # İstanbul (IETT)
55
108
  turkiyem sehir istanbul
56
109
  turkiyem hat 34AS
57
110
  ```
58
111
 
59
- Ankara icin EGO web sitesinden sefer saatleri cekilir.
60
- Istanbul icin once IETT SOAP Planlanan Sefer Saati servisi ile kalkis saatleri getirilir.
61
- SOAP servisi gecici olarak erisilemezse otomatik olarak GTFS ozet verisine dusulur.
112
+ İstanbul akışı:
113
+ - 1) GTFS hat özeti
114
+ - 2) SOAP planlanan sefer saatleri
115
+ - 3) SOAP erişilemezse GTFS özeti + bilgilendirme mesajı
116
+
117
+ ### IETT canlı konum
118
+
119
+ ```bash
120
+ # Özet çıktı
121
+ turkiyem hat canli 34AS
62
122
 
63
- Istanbul cikti sirasi:
64
- - Hat bilgileri (GTFS)
65
- - Planlanan sefer saatleri (SOAP)
66
- - SOAP hatasinda: yalnizca GTFS ozet + uyari
123
+ # Detay çıktı
124
+ turkiyem hat canli 34AS --detay
125
+ ```
67
126
 
68
- ### Deprem Sorgulama
127
+ > Not: Canlı konum sadece `istanbul` şehir seçiliyken anlamlıdır ve servis erişimine bağlıdır.
69
128
 
70
- AFAD API uzerinden gercek zamanli deprem verileri:
129
+ ### Deprem (AFAD)
71
130
 
72
131
  ```bash
73
- # Son 24 saat
74
132
  turkiyem deprem son24
75
-
76
- # Son 7 gun
77
133
  turkiyem deprem 7gun
78
-
79
- # Buyukluge gore filtrele (ornegin >= 4.0)
80
134
  turkiyem deprem buyukluk 4.0
81
135
  ```
82
136
 
83
- Buyuklugu 4.0 ve ustu olan depremler kirmizi ile vurgulanir.
84
-
85
- ### Hava Durumu ve Hava Kalitesi
86
-
87
- Open-Meteo uzerinden API key gerektirmeden sorgu yapar:
137
+ ### Hava durumu ve hava kalitesi (Open-Meteo)
88
138
 
89
139
  ```bash
90
- # Secili sehir icin guncel hava
140
+ # Seçili şehir
91
141
  turkiyem hava guncel
92
142
 
93
- # Sehir bazli guncel hava
143
+ # Şehir adıyla
94
144
  turkiyem hava guncel istanbul
145
+ turkiyem hava saatlik ankara --gun 3
146
+ turkiyem hava kalite izmir
95
147
 
96
- # Koordinat bazli guncel hava
148
+ # Koordinatla
97
149
  turkiyem hava guncel 41.0082,28.9784
150
+ ```
98
151
 
99
- # Saatlik tahmin (varsayilan 2 gun)
100
- turkiyem hava saatlik istanbul
152
+ ### Ekonomi (TCMB)
101
153
 
102
- # Saatlik tahmin gun sayisi (1-7)
103
- turkiyem hava saatlik istanbul --gun 3
154
+ ```bash
155
+ # Sık kullanılan kurlar (USD, EUR vb.)
156
+ turkiyem doviz
104
157
 
105
- # Hava kalitesi
106
- turkiyem hava kalite ankara
158
+ # Tüm kurlar listesi
159
+ turkiyem doviz --tum
107
160
  ```
108
161
 
109
- ### Temizleme
110
-
111
- Cache ve yapilandirmayi sifirlar:
112
-
113
- ```bash
114
- turkiyem temizle
162
+ ## Veri Kaynakları
163
+
164
+ | Kaynak | Kullanım |
165
+ |---|---|
166
+ | AFAD | Deprem verileri |
167
+ | EGO | Ankara hat/sefer saatleri |
168
+ | IETT GTFS | İstanbul hat özeti |
169
+ | IETT SOAP (PlanlananSeferSaati) | İstanbul planlanan sefer saatleri |
170
+ | IETT SOAP (SeferGerceklesme) | İstanbul canlı araç konumu |
171
+ | Open-Meteo Forecast | Güncel hava + saatlik tahmin |
172
+ | Open-Meteo Air Quality | Hava kalitesi |
173
+ | TCMB | Döviz kurları |
174
+
175
+ ## Mimari ve Proje Yapısı
176
+
177
+ ```text
178
+ src/
179
+ commands/ # CLI komut handler'ları
180
+ services/ # Dış API / scraping / SOAP katmanı
181
+ utils/ # Tablo, cache, config, banner yardımcıları
182
+ index.js # Commander giriş noktası
115
183
  ```
116
184
 
117
- ### Versiyon
185
+ Mimari prensipleri:
186
+ - Komut katmanı ile servis katmanı ayrımı
187
+ - API bağımlılıklarının servis içinde izole edilmesi
188
+ - Tekrarlanan işlerin util katmanına alınması
189
+ - Tüm dış isteklerde timeout + anlamlı hata mesajı
118
190
 
119
- ```bash
120
- turkiyem --version
121
- ```
191
+ ## Yapılandırma ve Cache
122
192
 
123
- ## Yapilandirma
193
+ - Seçili şehir dosyası:
194
+ - `~/.turkiyem/config.json`
195
+ - Varsayılan cache:
196
+ - Bellek içi (`node-cache`)
197
+ - Kaynak bazlı TTL (ör. IETT SOAP, hava durumu vb.)
124
198
 
125
- Secili sehir `~/.turkiyem/config.json` dosyasinda saklanir. Bu dosya otomatik olusturulur.
199
+ Temizleme:
126
200
 
127
- ## Veri Kaynaklari
201
+ ```bash
202
+ turkiyem temizle
203
+ ```
128
204
 
129
- | Kaynak | Aciklama |
130
- |--------|----------|
131
- | AFAD | Deprem verileri (deprem.afad.gov.tr) |
132
- | EGO | Ankara otobus sefer saatleri (ego.gov.tr) |
133
- | IETT | Istanbul GTFS hat verileri (data.ibb.gov.tr) |
134
- | IETT SOAP | Planlanan sefer saatleri (api.ibb.gov.tr) |
135
- | Open-Meteo | Guncel hava ve saatlik tahmin (api.open-meteo.com) |
136
- | Open-Meteo AQ | Hava kalitesi (air-quality-api.open-meteo.com) |
205
+ ## Hata Yönetimi
137
206
 
138
- ## npm Publish
207
+ Projede:
208
+ - `unhandledRejection` ve `uncaughtException` yakalanır
209
+ - API hataları kullanıcı dostu mesajlara çevrilir
210
+ - Ağ timeout / bağlantı sorunları için özel açıklamalar verilir
211
+ - Erişilemeyen kaynaklarda mümkün olan yerlerde fallback uygulanır
139
212
 
140
- 1. npm hesabiniza giris yapin:
213
+ ## Geliştirme
141
214
 
142
215
  ```bash
143
- npm login
216
+ npm install
217
+ npm start
144
218
  ```
145
219
 
146
- 2. package.json icindeki `name`, `version`, `author` alanlarini duzenleyin.
147
-
148
- 3. Yayinlayin:
220
+ Örnek geliştirme doğrulama komutları:
149
221
 
150
222
  ```bash
151
- npm publish
223
+ node src/index.js help
224
+ node src/index.js deprem son24
225
+ node src/index.js hava guncel istanbul
226
+ node src/index.js hat 34AS
152
227
  ```
153
228
 
154
- 4. Guncelleme icin versiyonu artirin:
229
+ ## Yayınlama (npm)
155
230
 
156
231
  ```bash
232
+ # 1) sürüm artır
157
233
  npm version patch
158
- npm publish
234
+
235
+ # 2) publish
236
+ npm publish --access public
237
+
238
+ # 3) kontrol
239
+ npm view turkiyem version
159
240
  ```
160
241
 
242
+ ## Sık Karşılaşılan Sorunlar
243
+
244
+ ### `npm publish` 403 (aynı sürüm)
245
+ Önceden yayınlanmış bir sürüm numarasını tekrar gönderemezsiniz.
246
+ Çözüm: `npm version patch|minor|major` sonrası tekrar publish.
247
+
248
+ ### `npm publish` 403 (2FA / token)
249
+ NPM hesabınız için 2FA veya granular token gereksinimi olabilir.
250
+ Çözüm: NPM hesabında token/2FA ayarlarını tamamlayın.
251
+
252
+ ### IETT canlı konum 500
253
+ SOAP servis tarafı geçici olarak hata döndürebilir.
254
+ Bu durumda kısa süre sonra tekrar deneyin.
255
+
161
256
  ## Lisans
162
257
 
163
258
  MIT
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "turkiyem",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Türkiye Toplu Taşıma ve Deprem CLI aracı - AFAD deprem verileri, EGO hat saatleri ve IETT SOAP/GTFS bilgileri",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "bin": {
8
- "turkiyem": "./src/index.js"
8
+ "turkiyem": "src/index.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node src/index.js",
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchExchangeRates } from '../services/tcmbService.js';
4
+ import { createDovizTable } from '../utils/display.js';
5
+
6
+ export async function dovizKurlari(options) {
7
+ const spinner = ora('TCMB döviz kurları alınıyor...').start();
8
+
9
+ try {
10
+ const result = await fetchExchangeRates();
11
+ spinner.succeed(`TCMB kurları alındı (Tarih: ${result.date || '-'})`);
12
+
13
+ const popular = ['USD', 'EUR', 'GBP', 'CHF', 'JPY', 'CAD', 'AUD', 'DKK', 'SEK', 'NOK'];
14
+ const kurlar = options.tum ? result.currencies : result.currencies.filter(c => popular.includes(c.kodu));
15
+
16
+ console.log('');
17
+ console.log(createDovizTable({ ...result, currencies: kurlar }));
18
+
19
+ if (!options.tum) {
20
+ console.log('');
21
+ console.log(chalk.gray(' Tüm kurları görmek için: turkiyem doviz --tum'));
22
+ }
23
+ } catch (err) {
24
+ spinner.fail(chalk.red(err.message));
25
+ }
26
+ }
@@ -2,9 +2,11 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { getCity } from '../utils/config.js';
4
4
  import { fetchEgoSchedule } from '../services/egoService.js';
5
- import { fetchIettRouteWithPlannedTimes } from '../services/iettService.js';
5
+ import { fetchIettLiveVehicles, fetchIettRouteWithPlannedTimes } from '../services/iettService.js';
6
6
  import {
7
7
  createEgoInfoTable,
8
+ createIettLiveDetailTable,
9
+ createIettLiveSummaryTable,
8
10
  createEgoScheduleTable,
9
11
  createIettPlannedTimesTable,
10
12
  createRouteTable,
@@ -34,6 +36,48 @@ export async function hatSorgula(hatNo) {
34
36
  }
35
37
  }
36
38
 
39
+ export async function hatCanliSorgula(hatNo, options = {}) {
40
+ if (!hatNo) {
41
+ console.log(chalk.red('Hat numarası belirtmelisiniz. Örnek: turkiyem hat canli 34AS'));
42
+ return;
43
+ }
44
+
45
+ const city = getCity();
46
+ if (!city) {
47
+ console.log(chalk.yellow('Henüz şehir seçmediniz. Önce şehir seçin:'));
48
+ console.log(chalk.cyan(' turkiyem sehir istanbul'));
49
+ return;
50
+ }
51
+
52
+ if (city !== 'istanbul') {
53
+ console.log(chalk.yellow('Canlı konum özelliği sadece İstanbul (IETT) için kullanılabilir.'));
54
+ console.log(chalk.gray('Önce şehir seçimini istanbul yapın: turkiyem sehir istanbul'));
55
+ return;
56
+ }
57
+
58
+ const spinner = ora(`IETT canlı araç konumları alınıyor (${hatNo})...`).start();
59
+
60
+ try {
61
+ const liveData = await fetchIettLiveVehicles(hatNo);
62
+ spinner.succeed(`IETT canlı konum verisi alındı (${liveData.summary.totalVehicles} araç)`);
63
+ console.log('');
64
+
65
+ console.log(chalk.white.bold(' IETT Canlı Konum Özeti'));
66
+ console.log(createIettLiveSummaryTable(liveData));
67
+
68
+ if (options.detay) {
69
+ console.log('');
70
+ console.log(chalk.white.bold(' IETT Canlı Konum Detayları'));
71
+ console.log(createIettLiveDetailTable(liveData));
72
+ } else {
73
+ console.log('');
74
+ console.log(chalk.gray(' Detay tablo için: turkiyem hat canli <numara> --detay'));
75
+ }
76
+ } catch (err) {
77
+ spinner.fail(chalk.red(err.message));
78
+ }
79
+ }
80
+
37
81
  async function queryEgo(hatNo) {
38
82
  const spinner = ora(`EGO hat ${hatNo} bilgileri alınıyor...`).start();
39
83
 
package/src/index.js CHANGED
@@ -5,10 +5,11 @@ import { Command } from 'commander';
5
5
  import { createRequire } from 'node:module';
6
6
  import { printBanner, printHelp } from './utils/banner.js';
7
7
  import { sehirSec } from './commands/sehir.js';
8
- import { hatSorgula } from './commands/hat.js';
8
+ import { hatCanliSorgula, hatSorgula } from './commands/hat.js';
9
9
  import { depremSon24, deprem7Gun, depremBuyukluk } from './commands/deprem.js';
10
10
  import { havaGuncel, havaKalitesi, havaSaatlik } from './commands/hava.js';
11
11
  import { temizle } from './commands/temizle.js';
12
+ import { dovizKurlari } from './commands/doviz.js';
12
13
 
13
14
  const require = createRequire(import.meta.url);
14
15
  const pkg = require('../package.json');
@@ -39,11 +40,20 @@ program
39
40
  sehirSec(sehir);
40
41
  });
41
42
 
42
- program
43
- .command('hat <numara>')
44
- .description('Hat bilgilerini sorgula')
45
- .action(async (numara) => {
46
- await hatSorgula(numara);
43
+ const hatCmd = program
44
+ .command('hat')
45
+ .description('Hat bilgilerini sorgula');
46
+
47
+ hatCmd
48
+ .argument('[arg1]', 'Hat numarası veya "canli"')
49
+ .argument('[arg2]', 'Canlı komutu için hat numarası')
50
+ .option('--detay', 'Araç bazlı detay tabloyu göster')
51
+ .action(async (arg1, arg2, options) => {
52
+ if (arg1 === 'canli') {
53
+ await hatCanliSorgula(arg2, options);
54
+ return;
55
+ }
56
+ await hatSorgula(arg1);
47
57
  });
48
58
 
49
59
  const depremCmd = program
@@ -104,6 +114,14 @@ program
104
114
  temizle();
105
115
  });
106
116
 
117
+ program
118
+ .command('doviz')
119
+ .description('TCMB güncel döviz kurlarını sorgula')
120
+ .option('--tum', 'Tüm kurları göster')
121
+ .action(async (options) => {
122
+ await dovizKurlari(options);
123
+ });
124
+
107
125
  program
108
126
  .command('help')
109
127
  .description('Yardım göster')
@@ -5,6 +5,8 @@ const DATASTORE_SQL_URL = 'https://data.ibb.gov.tr/api/3/action/datastore_search
5
5
  const DATASTORE_SEARCH_URL = 'https://data.ibb.gov.tr/api/3/action/datastore_search';
6
6
  const PLANNED_TIME_SOAP_URL = 'https://api.ibb.gov.tr/iett/UlasimAnaVeri/PlanlananSeferSaati.asmx';
7
7
  const PLANNED_TIME_SOAP_ACTION = 'http://tempuri.org/GetPlanlananSeferSaati_json';
8
+ const LIVE_SOAP_URL = 'https://api.ibb.gov.tr/iett/FiloDurum/SeferGerceklesme.asmx';
9
+ const LIVE_SOAP_ACTION = 'http://tempuri.org/GetHatOtoKonum_json';
8
10
 
9
11
  const RESOURCE_IDS = {
10
12
  routes: '46dbe388-c8c2-45c4-ac72-c06953de56a2',
@@ -170,6 +172,79 @@ function extractSoapResultJson(xmlText) {
170
172
  return JSON.parse(jsonText);
171
173
  }
172
174
 
175
+ function buildLiveSoapBody(routeCode) {
176
+ return `<?xml version="1.0" encoding="utf-8"?>
177
+ <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
178
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
179
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
180
+ <soap:Body>
181
+ <GetHatOtoKonum_json xmlns="http://tempuri.org/">
182
+ <HatNo>${routeCode}</HatNo>
183
+ </GetHatOtoKonum_json>
184
+ </soap:Body>
185
+ </soap:Envelope>`;
186
+ }
187
+
188
+ function extractLiveSoapResultJson(xmlText) {
189
+ const match = xmlText.match(
190
+ /<GetHatOtoKonum_jsonResult>([\s\S]*?)<\/GetHatOtoKonum_jsonResult>/i,
191
+ );
192
+
193
+ if (!match || !match[1]) return [];
194
+ const jsonText = match[1].trim();
195
+ if (!jsonText || jsonText === '[]') return [];
196
+ return JSON.parse(jsonText);
197
+ }
198
+
199
+ function normalizeLiveVehicles(records) {
200
+ return records.map((item) => ({
201
+ vehicleDoorNo: item.kapino || '-',
202
+ longitude: item.boylam || '-',
203
+ latitude: item.enlem || '-',
204
+ routeCode: item.hatkodu || '-',
205
+ routeVariantCode: item.guzergahkodu || '-',
206
+ routeName: item.hatad || '-',
207
+ direction: item.yon || '-',
208
+ lastLocationTime: item.son_konum_zamani || '-',
209
+ nearestStopCode: item.yakinDurakKodu || '-',
210
+ }));
211
+ }
212
+
213
+ function parseDateValue(value) {
214
+ if (!value || value === '-') return Number.NaN;
215
+ const normalized = value.includes('T') ? value : value.replace(' ', 'T');
216
+ const ts = new Date(normalized).getTime();
217
+ return Number.isNaN(ts) ? Number.NaN : ts;
218
+ }
219
+
220
+ function buildLiveSummary(vehicles) {
221
+ const directionCounter = new Map();
222
+ let latestTime = '-';
223
+ let latestTs = Number.NEGATIVE_INFINITY;
224
+
225
+ for (const vehicle of vehicles) {
226
+ const direction = vehicle.direction || '-';
227
+ directionCounter.set(direction, (directionCounter.get(direction) || 0) + 1);
228
+
229
+ const ts = parseDateValue(vehicle.lastLocationTime);
230
+ if (!Number.isNaN(ts) && ts > latestTs) {
231
+ latestTs = ts;
232
+ latestTime = vehicle.lastLocationTime;
233
+ }
234
+ }
235
+
236
+ const directionDistribution = Array.from(directionCounter.entries()).map(([direction, count]) => ({
237
+ direction,
238
+ count,
239
+ }));
240
+
241
+ return {
242
+ totalVehicles: vehicles.length,
243
+ latestLocationTime: latestTime,
244
+ directionDistribution,
245
+ };
246
+ }
247
+
173
248
  export async function fetchIettPlannedTimes(routeCode) {
174
249
  const code = routeCode.toUpperCase();
175
250
  const cacheKey = `iett_soap_times_${code}`;
@@ -317,3 +392,64 @@ export async function fetchIettRouteWithPlannedTimes(routeCode) {
317
392
  };
318
393
  }
319
394
  }
395
+
396
+ export async function fetchIettLiveVehicles(routeCode) {
397
+ const code = routeCode.toUpperCase();
398
+ const cacheKey = `iett_live_${code}`;
399
+ const cached = getCached(cacheKey);
400
+ if (cached) return cached;
401
+
402
+ const body = buildLiveSoapBody(code);
403
+
404
+ try {
405
+ const response = await axios.post(LIVE_SOAP_URL, body, {
406
+ timeout: 15000,
407
+ headers: {
408
+ 'Content-Type': 'text/xml; charset=utf-8',
409
+ SOAPAction: LIVE_SOAP_ACTION,
410
+ ...API_HEADERS,
411
+ },
412
+ responseType: 'text',
413
+ maxRedirects: 5,
414
+ validateStatus: (status) => status >= 200 && status < 600,
415
+ });
416
+
417
+ if (response.status === 403) {
418
+ throw new Error('IETT canlı konum servisi erişimi engellendi (403).');
419
+ }
420
+ if (response.status >= 500) {
421
+ throw new Error(`IETT canlı konum servisi şu anda erişilemiyor (HTTP ${response.status}).`);
422
+ }
423
+ if (response.status !== 200) {
424
+ throw new Error(`IETT canlı konum servisi HTTP ${response.status} döndürdü.`);
425
+ }
426
+
427
+ const parsed = extractLiveSoapResultJson(response.data);
428
+ const vehicles = normalizeLiveVehicles(parsed);
429
+
430
+ if (vehicles.length === 0) {
431
+ throw new Error(`"${routeCode}" hattı için aktif araç konumu bulunamadı.`);
432
+ }
433
+
434
+ const summary = buildLiveSummary(vehicles);
435
+ const result = {
436
+ routeCode: code,
437
+ summary,
438
+ vehicles,
439
+ };
440
+
441
+ setCached(cacheKey, result, CACHE_TTL.IETT_LIVE);
442
+ return result;
443
+ } catch (err) {
444
+ if (err.code === 'ECONNABORTED') {
445
+ throw new Error('IETT canlı konum servisi zaman aşımına uğradı.');
446
+ }
447
+ if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
448
+ throw new Error('IETT canlı konum servisine bağlanılamadı.');
449
+ }
450
+ if (err instanceof SyntaxError) {
451
+ throw new Error('IETT canlı konum yanıtı çözümlenemedi.');
452
+ }
453
+ throw err;
454
+ }
455
+ }
@@ -0,0 +1,47 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+ import { getCached, setCached } from '../utils/cache.js';
4
+
5
+ const CACHE_KEY = 'tcmb_kur';
6
+ const CACHE_TTL = 3600; // 1 saat
7
+ const URL = 'https://www.tcmb.gov.tr/kurlar/today.xml';
8
+
9
+ export async function fetchExchangeRates() {
10
+ const cached = getCached(CACHE_KEY);
11
+ if (cached) return cached;
12
+
13
+ try {
14
+ const response = await axios.get(URL, { timeout: 10000 });
15
+ const $ = cheerio.load(response.data, { xmlMode: true });
16
+
17
+ const date = $('Tarih_Date').attr('Tarih');
18
+ const bultenNo = $('Tarih_Date').attr('Bulten_No');
19
+
20
+ const currencies = [];
21
+
22
+ $('Currency').each((_, el) => {
23
+ const isim = $(el).find('Isim').text().trim();
24
+ const code = $(el).attr('CurrencyCode');
25
+ const alis = $(el).find('ForexBuying').text().trim();
26
+ const satis = $(el).find('ForexSelling').text().trim();
27
+
28
+ if (isim && code) {
29
+ currencies.push({
30
+ isim,
31
+ kodu: code,
32
+ alis,
33
+ satis
34
+ });
35
+ }
36
+ });
37
+
38
+ const result = { date, bultenNo, currencies };
39
+ setCached(CACHE_KEY, result, CACHE_TTL);
40
+ return result;
41
+ } catch (error) {
42
+ if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
43
+ throw new Error('TCMB servisine erişilemedi (Zaman aşımı).');
44
+ }
45
+ throw new Error(`TCMB veri hatası: ${error.message}`);
46
+ }
47
+ }
@@ -22,12 +22,14 @@ export function printHelp() {
22
22
  console.log(chalk.white.bold(' Komutlar:\n'));
23
23
  console.log(chalk.cyan(' turkiyem sehir <ankara|istanbul>') + chalk.gray(' Şehir seç'));
24
24
  console.log(chalk.cyan(' turkiyem hat <numara>') + chalk.gray(' Hat sorgula'));
25
+ console.log(chalk.cyan(' turkiyem hat canli <numara> [--detay]') + chalk.gray(' IETT canlı konum'));
25
26
  console.log(chalk.cyan(' turkiyem hava guncel [sehir|lat,lon]') + chalk.gray(' Güncel hava'));
26
27
  console.log(chalk.cyan(' turkiyem hava saatlik [sehir|lat,lon] -g 2') + chalk.gray(' Saatlik tahmin'));
27
28
  console.log(chalk.cyan(' turkiyem hava kalite [sehir|lat,lon]') + chalk.gray(' Hava kalitesi'));
28
29
  console.log(chalk.cyan(' turkiyem deprem son24') + chalk.gray(' Son 24 saat depremler'));
29
30
  console.log(chalk.cyan(' turkiyem deprem 7gun') + chalk.gray(' Son 7 gün depremler'));
30
31
  console.log(chalk.cyan(' turkiyem deprem buyukluk <deger>') + chalk.gray(' Büyüklüğe göre filtrele'));
32
+ console.log(chalk.cyan(' turkiyem doviz [--tum]') + chalk.gray(' TCMB Döviz kurları'));
31
33
  console.log(chalk.cyan(' turkiyem temizle') + chalk.gray(' Cache ve config temizle'));
32
34
  console.log(chalk.cyan(' turkiyem --version') + chalk.gray(' Versiyon göster'));
33
35
  console.log('');
@@ -3,6 +3,7 @@ import NodeCache from 'node-cache';
3
3
  export const CACHE_TTL = Object.freeze({
4
4
  DEFAULT: 300,
5
5
  IETT_SOAP: 90,
6
+ IETT_LIVE: 45,
6
7
  WEATHER_CURRENT: 300,
7
8
  WEATHER_HOURLY: 600,
8
9
  WEATHER_AIR: 600,
@@ -198,3 +198,77 @@ export function createAirQualityTable(result) {
198
198
 
199
199
  return table.toString();
200
200
  }
201
+
202
+ export function createIettLiveSummaryTable(liveData) {
203
+ const table = new Table({
204
+ style: { head: [], border: ['gray'] },
205
+ wordWrap: true,
206
+ });
207
+
208
+ const directionText = (liveData.summary?.directionDistribution || [])
209
+ .map((item) => `${item.direction}: ${item.count}`)
210
+ .join(' | ');
211
+
212
+ table.push(
213
+ { [chalk.cyan('Hat Kodu')]: liveData.routeCode || '-' },
214
+ { [chalk.cyan('Aktif Araç Sayısı')]: String(liveData.summary?.totalVehicles ?? 0) },
215
+ { [chalk.cyan('Yön Dağılımı')]: directionText || '-' },
216
+ { [chalk.cyan('Son Konum Zamanı')]: liveData.summary?.latestLocationTime || '-' },
217
+ );
218
+
219
+ return table.toString();
220
+ }
221
+
222
+ export function createIettLiveDetailTable(liveData) {
223
+ const table = new Table({
224
+ head: [
225
+ chalk.white.bold('Kapı No'),
226
+ chalk.white.bold('Yön'),
227
+ chalk.white.bold('Enlem'),
228
+ chalk.white.bold('Boylam'),
229
+ chalk.white.bold('Yakın Durak'),
230
+ chalk.white.bold('Son Konum Zamanı'),
231
+ ],
232
+ colWidths: [10, 24, 12, 12, 14, 22],
233
+ style: { head: [], border: ['gray'] },
234
+ wordWrap: true,
235
+ });
236
+
237
+ for (const vehicle of liveData.vehicles || []) {
238
+ table.push([
239
+ vehicle.vehicleDoorNo || '-',
240
+ vehicle.direction || '-',
241
+ String(vehicle.latitude ?? '-'),
242
+ String(vehicle.longitude ?? '-'),
243
+ vehicle.nearestStopCode || '-',
244
+ vehicle.lastLocationTime || '-',
245
+ ]);
246
+ }
247
+
248
+ return table.toString();
249
+ }
250
+
251
+ export function createDovizTable(result) {
252
+ const table = new Table({
253
+ head: [
254
+ chalk.white.bold('Kod'),
255
+ chalk.white.bold('Döviz Cinsi'),
256
+ chalk.white.bold('Alış (TL)'),
257
+ chalk.white.bold('Satış (TL)'),
258
+ ],
259
+ colWidths: [8, 30, 15, 15],
260
+ style: { head: [], border: ['gray'] },
261
+ });
262
+
263
+ for (const c of result.currencies) {
264
+ if (!c.alis && !c.satis) continue;
265
+ table.push([
266
+ chalk.cyan(c.kodu),
267
+ c.isim,
268
+ c.alis || '-',
269
+ c.satis || '-'
270
+ ]);
271
+ }
272
+
273
+ return table.toString();
274
+ }